diff --git a/.browserslistrc b/.browserslistrc
index 54dd3aaf34..0376af4bcc 100644
--- a/.browserslistrc
+++ b/.browserslistrc
@@ -1,7 +1,9 @@
[production]
defaults
-not IE 11
+> 0.2%
+ios >= 15.6
not dead
+not OperaMini all
[development]
supports es6-module
diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json
index b32e4026d2..ca9156fdaa 100644
--- a/.devcontainer/codespaces/devcontainer.json
+++ b/.devcontainer/codespaces/devcontainer.json
@@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
- "ghcr.io/devcontainers/features/sshd:1": {},
+ "ghcr.io/devcontainers/features/sshd:1": {}
},
"runServices": ["app", "db", "redis"],
@@ -15,16 +15,16 @@
"portsAttributes": {
"3000": {
"label": "web",
- "onAutoForward": "notify",
+ "onAutoForward": "notify"
},
"4000": {
"label": "stream",
- "onAutoForward": "silent",
- },
+ "onAutoForward": "silent"
+ }
},
"otherPortsAttributes": {
- "onAutoForward": "silent",
+ "onAutoForward": "silent"
},
"remoteEnv": {
@@ -33,7 +33,7 @@
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
"DISABLE_FORGERY_REQUEST_PROTECTION": "true",
"ES_ENABLED": "",
- "LIBRE_TRANSLATE_ENDPOINT": "",
+ "LIBRE_TRANSLATE_ENDPOINT": ""
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@@ -43,7 +43,7 @@
"customizations": {
"vscode": {
"settings": {},
- "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
- },
- },
+ "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
+ }
+ }
}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ed71235b3b..fa8d6542c1 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
- "ghcr.io/devcontainers/features/sshd:1": {},
+ "ghcr.io/devcontainers/features/sshd:1": {}
},
"forwardPorts": [3000, 4000],
@@ -14,17 +14,17 @@
"3000": {
"label": "web",
"onAutoForward": "notify",
- "requireLocalPort": true,
+ "requireLocalPort": true
},
"4000": {
"label": "stream",
"onAutoForward": "silent",
- "requireLocalPort": true,
- },
+ "requireLocalPort": true
+ }
},
"otherPortsAttributes": {
- "onAutoForward": "silent",
+ "onAutoForward": "silent"
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@@ -34,7 +34,7 @@
"customizations": {
"vscode": {
"settings": {},
- "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
- },
- },
+ "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
+ }
+ }
}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index ecdf9f5f53..d14af5d7d9 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -70,7 +70,7 @@ services:
hard: -1
libretranslate:
- image: libretranslate/libretranslate:v1.5.5
+ image: libretranslate/libretranslate:v1.5.6
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local
diff --git a/.eslintrc.js b/.eslintrc.js
index ae13b0711c..87a1af483b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -123,7 +123,7 @@ module.exports = defineConfig({
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error',
- // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js
+ // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
@@ -176,7 +176,7 @@ module.exports = defineConfig({
},
],
- // See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js
+ // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
'import/extensions': [
'error',
'always',
@@ -355,7 +355,6 @@ module.exports = defineConfig({
'plugin:import/typescript',
'plugin:promise/recommended',
'plugin:jsdoc/recommended-typescript',
- 'plugin:prettier/recommended',
],
parserOptions: {
@@ -364,6 +363,9 @@ module.exports = defineConfig({
},
rules: {
+ // Disable formatting rules that have been enabled in the base config
+ 'indent': 'off',
+
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
diff --git a/.github/codecov.yml b/.github/codecov.yml
index 5532c49618..9d6413a106 100644
--- a/.github/codecov.yml
+++ b/.github/codecov.yml
@@ -1,3 +1,4 @@
+comment: false # Do not leave PR comments
coverage:
status:
project:
@@ -8,6 +9,3 @@ coverage:
default:
# Github status check is not blocking
informational: true
-comment:
- # Only write a comment in PR if there are changes
- require_changes: true
diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml
new file mode 100644
index 0000000000..2d483b5022
--- /dev/null
+++ b/.github/workflows/format-check.yml
@@ -0,0 +1,18 @@
+name: Check formatting
+on:
+ push:
+ pull_request:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Set up Javascript environment
+ uses: ./.github/actions/setup-javascript
+
+ - name: Check formatting with Prettier
+ run: yarn format:check
diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml
index 7229bec582..e5f4874877 100644
--- a/.github/workflows/lint-css.yml
+++ b/.github/workflows/lint-css.yml
@@ -43,4 +43,4 @@ jobs:
- run: echo "::add-matcher::.github/stylelint-matcher.json"
- name: Stylelint
- run: yarn lint:sass
+ run: yarn lint:css
diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml
deleted file mode 100644
index 7796bf92c4..0000000000
--- a/.github/workflows/lint-json.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: JSON Linting
-on:
- push:
- branches-ignore:
- - 'dependabot/**'
- - 'renovate/**'
- paths:
- - 'package.json'
- - 'yarn.lock'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.json'
- - '.github/workflows/lint-json.yml'
- - '!app/javascript/mastodon/locales/*.json'
-
- pull_request:
- paths:
- - 'package.json'
- - 'yarn.lock'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.json'
- - '.github/workflows/lint-json.yml'
- - '!app/javascript/mastodon/locales/*.json'
-
-jobs:
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - name: Clone repository
- uses: actions/checkout@v4
-
- - name: Set up Javascript environment
- uses: ./.github/actions/setup-javascript
-
- - name: Prettier
- run: yarn lint:json
diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml
deleted file mode 100644
index 51c59937a3..0000000000
--- a/.github/workflows/lint-md.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Markdown Linting
-on:
- push:
- branches-ignore:
- - 'dependabot/**'
- - 'renovate/**'
- paths:
- - '.github/workflows/lint-md.yml'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.md'
- - '!AUTHORS.md'
- - 'package.json'
- - 'yarn.lock'
-
- pull_request:
- paths:
- - '.github/workflows/lint-md.yml'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.md'
- - '!AUTHORS.md'
- - 'package.json'
- - 'yarn.lock'
-
-jobs:
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - name: Clone repository
- uses: actions/checkout@v4
-
- - name: Set up Javascript environment
- uses: ./.github/actions/setup-javascript
-
- - name: Prettier
- run: yarn lint:md
diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml
deleted file mode 100644
index 908bdef5cc..0000000000
--- a/.github/workflows/lint-yml.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: YML Linting
-on:
- push:
- branches-ignore:
- - 'dependabot/**'
- - 'renovate/**'
- paths:
- - 'package.json'
- - 'yarn.lock'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.yaml'
- - '**/*.yml'
- - '.github/workflows/lint-yml.yml'
- - '!config/locales/*.yml'
-
- pull_request:
- paths:
- - 'package.json'
- - 'yarn.lock'
- - '.nvmrc'
- - '.prettier*'
- - '**/*.yaml'
- - '**/*.yml'
- - '.github/workflows/lint-yml.yml'
- - '!config/locales/*.yml'
-
-jobs:
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - name: Clone repository
- uses: actions/checkout@v4
-
- - name: Set up Javascript environment
- uses: ./.github/actions/setup-javascript
-
- - name: Prettier
- run: yarn lint:yml
diff --git a/.haml-lint.yml b/.haml-lint.yml
index 8cfcaec8d9..b94eb8b0df 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -14,3 +14,5 @@ linters:
enabled: true
LineLength:
max: 320
+ ViewLength:
+ max: 200 # Override default value of 100 inherited from rubocop
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
deleted file mode 100644
index af2d2e8f4e..0000000000
--- a/.haml-lint_todo.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-# This configuration was generated by
-# `haml-lint --auto-gen-config`
-# on 2024-01-09 11:30:07 -0500 using Haml-Lint version 0.53.0.
-# The point is for the user to remove these configuration records
-# one by one as the lints are removed from the code base.
-# Note that changes in the inspected code, or installation of new
-# versions of Haml-Lint, may require this file to be generated again.
-
-linters:
- # Offense count: 1
- LineLength:
- exclude:
- - 'app/views/admin/roles/_form.html.haml'
diff --git a/.prettierignore b/.prettierignore
index 2decee1935..d055085b25 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -54,6 +54,13 @@
# Ignore Docker option files
docker-compose.override.yml
+# Ignore public
+/public/assets
+/public/emoji
+/public/packs
+/public/packs-test
+/public/system
+
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
@@ -74,6 +81,7 @@ app/javascript/styles/mastodon/reset.scss
# Ignore the generated AUTHORS.md
AUTHORS.md
+# Process a few selected JS files
!lint-staged.config.js
# Ignore glitch-soc emoji map file
diff --git a/Dockerfile b/Dockerfile
index 119c266b89..8778133e0d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,7 +20,7 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something
-# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
+# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA=""
@@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler
-# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
+# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"
diff --git a/Gemfile b/Gemfile
index c137cd2bce..f2cf820b21 100644
--- a/Gemfile
+++ b/Gemfile
@@ -59,6 +59,7 @@ gem 'http', '~> 5.1'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.6.2'
gem 'idn-ruby', require: 'idn'
+gem 'inline_svg'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
@@ -112,7 +113,7 @@ group :test do
# RSpec helpers for email specs
gem 'email_spec'
- # Extra RSpec extenion methods and helpers for sidekiq
+ # Extra RSpec extension methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing
diff --git a/Gemfile.lock b/Gemfile.lock
index 076cf915d0..9c5bb940bc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -350,14 +350,17 @@ GEM
rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1)
idn-ruby (0.1.5)
+ inline_svg (1.9.0)
+ activesupport (>= 3.0)
+ nokogiri (>= 1.6)
io-console (0.7.2)
- irb (1.11.2)
+ irb (1.12.0)
rdoc
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.1)
json-canonicalization (1.0.0)
- json-jwt (1.15.3)
+ json-jwt (1.15.3.1)
activesupport (>= 4.2)
aes_key_wrap
bindata
@@ -372,7 +375,7 @@ GEM
json-ld-preloaded (3.3.0)
json-ld (~> 3.3)
rdf (~> 3.3)
- json-schema (4.1.1)
+ json-schema (4.2.0)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
@@ -455,7 +458,7 @@ GEM
net-smtp (0.4.0.1)
net-protocol
nio4r (2.5.9)
- nokogiri (1.16.2)
+ nokogiri (1.16.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.3.0)
@@ -465,11 +468,11 @@ GEM
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3)
bigdecimal (>= 3.0)
- omniauth (2.1.1)
+ omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
- omniauth-cas (3.0.0.beta.1)
+ omniauth-cas (3.0.0)
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
@@ -505,7 +508,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
- pg (1.5.5)
+ pg (1.5.6)
pghero (3.4.1)
activerecord (>= 6)
posix-spawn (0.3.15)
@@ -535,7 +538,7 @@ GEM
rack (2.2.8.1)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
- rack-cors (2.0.1)
+ rack-cors (2.0.2)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
activesupport
@@ -543,8 +546,9 @@ GEM
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
- rack-protection (3.0.5)
- rack
+ rack-protection (3.2.0)
+ base64 (>= 0.1.0)
+ rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
rack-session (1.0.2)
@@ -606,7 +610,7 @@ GEM
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.0)
- reline (0.4.2)
+ reline (0.4.3)
io-console (~> 0.5)
request_store (1.5.1)
rack (>= 1.4)
@@ -621,16 +625,16 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
- rspec-core (3.12.2)
- rspec-support (~> 3.12.0)
- rspec-expectations (3.12.3)
+ rspec-core (3.13.0)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.12.0)
+ rspec-support (~> 3.13.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
- rspec-mocks (3.12.6)
+ rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.12.0)
+ rspec-support (~> 3.13.0)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
@@ -644,7 +648,7 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
- rspec-support (3.12.1)
+ rspec-support (3.13.1)
rubocop (1.60.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
@@ -743,7 +747,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
- test-prof (1.3.1)
+ test-prof (1.3.2)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
@@ -864,6 +868,7 @@ DEPENDENCIES
httplog (~> 1.6.2)
i18n-tasks (~> 1.0)
idn-ruby
+ inline_svg
irb (~> 1.8)
json-ld
json-ld-preloaded (~> 3.2)
diff --git a/README.md b/README.md
index f10a09191f..c3ed938710 100644
--- a/README.md
+++ b/README.md
@@ -17,3 +17,151 @@ We also recommend applying wide custom emoji CSS for better experience.
# Thanks to...
- Freeplay for [mastodon-modern](https://codeberg.org/Freeplay/Mastodon-Modern/src/branch/main) theme
+
+
+- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
+- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).
+
+Mastodon Glitch-YRYR is a fork of Mastodon Glitch Edition which is a fork of [Mastodon](https://github.com/mastodon/mastodon). Up-Upstream's README file is reproduced below.
+
+---
+
+
+
+[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases]
+[![Ruby Testing](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml)
+[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
+
+[releases]: https://github.com/mastodon/mastodon/releases
+[crowdin]: https://crowdin.com/project/mastodon
+
+Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
+
+Click below to **learn more** in a video:
+
+[![Screenshot](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/ezgif-2-60f1b00403.gif)][youtube_demo]
+
+[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE
+
+## Navigation
+
+- [Project homepage 🐘](https://joinmastodon.org)
+- [Support the development via Patreon][patreon]
+- [View sponsors](https://joinmastodon.org/sponsors)
+- [Blog](https://blog.joinmastodon.org)
+- [Documentation](https://docs.joinmastodon.org)
+- [Roadmap](https://joinmastodon.org/roadmap)
+- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
+- [Browse Mastodon servers](https://joinmastodon.org/communities)
+- [Browse Mastodon apps](https://joinmastodon.org/apps)
+
+[patreon]: https://www.patreon.com/mastodon
+
+## Features
+
+
+
+### No vendor lock-in: Fully interoperable with any conforming platform
+
+It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
+
+### Real-time, chronological timeline updates
+
+Updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
+
+### Media attachments like images and short videos
+
+Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
+
+### Safety and moderation tools
+
+Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
+
+### OAuth2 and a straightforward REST API
+
+Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
+
+## Deployment
+
+### Tech stack
+
+- **Ruby on Rails** powers the REST API and other web pages
+- **React.js** and Redux are used for the dynamic parts of the interface
+- **Node.js** powers the streaming API
+
+### Requirements
+
+- **PostgreSQL** 12+
+- **Redis** 4+
+- **Ruby** 3.0+
+- **Node.js** 16+
+
+The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
+
+## Development
+
+### Vagrant
+
+A **Vagrant** configuration is included for development purposes. To use it, complete the following steps:
+
+- Install Vagrant and Virtualbox
+- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater`
+- Run `vagrant up`
+- Run `vagrant ssh -c "cd /vagrant && bin/dev"`
+- Open `http://mastodon.local` in your browser
+
+### MacOS
+
+To set up **MacOS** for native development, complete the following steps:
+
+- Use a Ruby version manager to install the specified version from `.ruby-version`
+- Run `brew install postgresql@14 redis imagemagick libidn` to install required dependencies
+- Navigate to Mastodon's root directory and run `brew install nvm` then `nvm use` to use the version from `.nvmrc`
+- Run `corepack enable && corepack prepare`
+- Run `bundle exec rails db:setup` (optionally prepend `RAILS_ENV=development` to target the dev environment)
+- Finally, run `bin/dev` which will launch the local services via `overmind` (if installed) or `foreman`
+
+### Docker
+
+For development with **Docker**, complete the following steps:
+
+- Install Docker Desktop
+- Run `docker compose -f .devcontainer/docker-compose.yml up -d`
+- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh`
+- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app bin/dev`
+
+If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers).
+
+### GitHub Codespaces
+
+To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project..
+
+- Click this button to create a new codespace:
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json)
+- Wait for the environment to build. This will take a few minutes.
+- When the editor is ready, run `bin/dev` in the terminal.
+- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon.
+- On the _Ports_ tab, right click on the “stream” row and select _Port visibility_ → _Public_.
+
+## Contributing
+
+Mastodon is **free, open-source software** licensed under **AGPLv3**.
+
+You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
+
+**IRC channel**: #mastodon on irc.libera.chat
+
+## License
+
+Copyright (C) 2016-2024 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
+
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
+>>>>>>> 3341db939cd077820ad598b0445d02ab2382eaf4
diff --git a/Vagrantfile b/Vagrantfile
index 6f0f511095..12bd1ba67a 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.post_up_message = < { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb
index d2942104e5..392dd36bcd 100644
--- a/app/controllers/activitypub/followers_synchronizations_controller.rb
+++ b/app/controllers/activitypub/followers_synchronizations_controller.rb
@@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
- include SignatureVerification
- include AccountOwnedConcern
-
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index e8b0f47cde..49cfc8ad1c 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < ActivityPub::BaseController
- include SignatureVerification
include JsonLdHelper
- include AccountOwnedConcern
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index bf10ba762a..8079e011dd 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -3,9 +3,6 @@
class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20
- include SignatureVerification
- include AccountOwnedConcern
-
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index c38ff89d1c..3f43e89a5e 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
- include SignatureVerification
include Authorization
- include AccountOwnedConcern
DESCENDANTS_LIMIT = 60
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 9beb8fde6b..d3be7817ff 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -128,7 +128,7 @@ module Admin
def unblock_email
authorize @account, :unblock_email?
- CanonicalEmailBlock.where(reference_account: @account).delete_all
+ CanonicalEmailBlock.matching_account(@account).delete_all
log_action :unblock_email, @account
diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb
index d31aec6ea8..b8def22ba3 100644
--- a/app/controllers/admin/rules_controller.rb
+++ b/app/controllers/admin/rules_controller.rb
@@ -53,7 +53,7 @@ module Admin
end
def resource_params
- params.require(:rule).permit(:text, :priority)
+ params.require(:rule).permit(:text, :hint, :priority)
end
end
end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 98fa1897ef..f87d596ce3 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
include Api::AccessTokenTrackingConcern
include Api::CachingConcern
include Api::ContentSecurityPolicy
+ include Api::ErrorHandling
skip_before_action :require_functional!, unless: :limited_federation_mode?
@@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session
- rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
- render json: { error: e.to_s }, status: 422
- end
-
- rescue_from ActiveRecord::RecordNotUnique do
- render json: { error: 'Duplicate record' }, status: 422
- end
-
- rescue_from Date::Error do
- render json: { error: 'Invalid date supplied' }, status: 422
- end
-
- rescue_from ActiveRecord::RecordNotFound do
- render json: { error: 'Record not found' }, status: 404
- end
-
- rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
- render json: { error: 'Remote data could not be fetched' }, status: 503
- end
-
- rescue_from OpenSSL::SSL::SSLError do
- render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
- end
-
- rescue_from Mastodon::NotPermittedError do
- render json: { error: 'This action is not allowed' }, status: 403
- end
-
- rescue_from Seahorse::Client::NetworkingError do |e|
- Rails.logger.warn "Storage server error: #{e}"
- render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
- end
-
- rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
- render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
- end
-
- rescue_from Mastodon::RateLimitExceededError do
- render json: { error: I18n.t('errors.429') }, status: 429
- end
-
- rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
- render json: { error: e.to_s }, status: 400
- end
-
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: error.try(:description) || 'Not authorized' } }
end
@@ -73,6 +29,14 @@ class Api::BaseController < ApplicationController
protected
+ def pagination_max_id
+ pagination_collection.last.id
+ end
+
+ def pagination_since_id
+ pagination_collection.first.id
+ end
+
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
@@ -140,6 +104,10 @@ class Api::BaseController < ApplicationController
private
+ def insert_pagination_headers
+ set_pagination_headers(next_path, prev_path)
+ end
+
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index f60181f1eb..449866fa55 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -41,10 +41,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_account_followers_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 3ab8c1efd6..c4f4313f8f 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -41,10 +41,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index fe4279302f..35ea9c8ec1 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -4,7 +4,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_account
- after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
+ after_action :insert_pagination_headers
def index
cache_if_unauthenticated!
@@ -35,10 +35,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -51,11 +47,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
- def pagination_max_id
- @statuses.last.id
- end
-
- def pagination_since_id
- @statuses.first.id
+ def pagination_collection
+ @statuses
end
end
diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb
index ff9cae6398..ff6f41e01d 100644
--- a/app/controllers/api/v1/admin/accounts_controller.rb
+++ b/app/controllers/api/v1/admin/accounts_controller.rb
@@ -125,10 +125,6 @@ class Api::V1::Admin::AccountsController < Api::BaseController
translated_params
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -137,12 +133,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
- def pagination_max_id
- @accounts.last.id
- end
-
- def pagination_since_id
- @accounts.first.id
+ def pagination_collection
+ @accounts
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
index 7b192b979f..701f668de6 100644
--- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
@@ -65,10 +65,6 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
@canonical_email_block = CanonicalEmailBlock.find(params[:id])
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -77,12 +73,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end
- def pagination_max_id
- @canonical_email_blocks.last.id
- end
-
- def pagination_since_id
- @canonical_email_blocks.first.id
+ def pagination_collection
+ @canonical_email_blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb
index dd54d67106..a7ae84e306 100644
--- a/app/controllers/api/v1/admin/domain_allows_controller.rb
+++ b/app/controllers/api/v1/admin/domain_allows_controller.rb
@@ -61,10 +61,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
DomainAllow.all
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -73,12 +69,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
end
- def pagination_max_id
- @domain_allows.last.id
- end
-
- def pagination_since_id
- @domain_allows.first.id
+ def pagination_collection
+ @domain_allows
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb
index 2538c7c7c2..b589d277d5 100644
--- a/app/controllers/api/v1/admin/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb
@@ -72,10 +72,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -84,12 +80,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
end
- def pagination_max_id
- @domain_blocks.last.id
- end
-
- def pagination_since_id
- @domain_blocks.first.id
+ def pagination_collection
+ @domain_blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
index df54b9f0a4..bdedb9d040 100644
--- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
@@ -58,10 +58,6 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
params.permit(:domain, :allow_with_approval)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -70,12 +66,8 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end
- def pagination_max_id
- @email_domain_blocks.last.id
- end
-
- def pagination_since_id
- @email_domain_blocks.first.id
+ def pagination_collection
+ @email_domain_blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb
index 61c1912344..3625781149 100644
--- a/app/controllers/api/v1/admin/ip_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb
@@ -63,10 +63,6 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
params.permit(:ip, :severity, :comment, :expires_in)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -75,12 +71,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end
- def pagination_max_id
- @ip_blocks.last.id
- end
-
- def pagination_since_id
- @ip_blocks.first.id
+ def pagination_collection
+ @ip_blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb
index 7129a5f6ca..9b5beeab67 100644
--- a/app/controllers/api/v1/admin/reports_controller.rb
+++ b/app/controllers/api/v1/admin/reports_controller.rb
@@ -89,10 +89,6 @@ class Api::V1::Admin::ReportsController < Api::BaseController
params.permit(*FILTER_PARAMS)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -101,12 +97,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end
- def pagination_max_id
- @reports.last.id
- end
-
- def pagination_since_id
- @reports.first.id
+ def pagination_collection
+ @reports
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb
index 6a7c9f5bf3..c754980720 100644
--- a/app/controllers/api/v1/admin/tags_controller.rb
+++ b/app/controllers/api/v1/admin/tags_controller.rb
@@ -44,10 +44,6 @@ class Api::V1::Admin::TagsController < Api::BaseController
params.permit(:display_name, :trendable, :usable, :listable)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -56,12 +52,8 @@ class Api::V1::Admin::TagsController < Api::BaseController
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
end
- def pagination_max_id
- @tags.last.id
- end
-
- def pagination_since_id
- @tags.first.id
+ def pagination_collection
+ @tags
end
def records_continue?
diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb
index 5d9fcc82c0..8bb5e22716 100644
--- a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb
+++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb
@@ -42,10 +42,6 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
@providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@@ -54,12 +50,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
end
- def pagination_max_id
- @providers.last.id
- end
-
- def pagination_since_id
- @providers.first.id
+ def pagination_collection
+ @providers
end
def records_continue?
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index 0934622f88..234ab2e82c 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -28,10 +28,6 @@ class Api::V1::BlocksController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_blocks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -40,12 +36,8 @@ class Api::V1::BlocksController < Api::BaseController
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
end
- def pagination_max_id
- paginated_blocks.last.id
- end
-
- def pagination_since_id
- paginated_blocks.first.id
+ def pagination_collection
+ paginated_blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb
index 498eb16f44..b6bb987b6b 100644
--- a/app/controllers/api/v1/bookmarks_controller.rb
+++ b/app/controllers/api/v1/bookmarks_controller.rb
@@ -31,10 +31,6 @@ class Api::V1::BookmarksController < Api::BaseController
current_account.bookmarks
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -43,12 +39,8 @@ class Api::V1::BookmarksController < Api::BaseController
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
- def pagination_max_id
- results.last.id
- end
-
- def pagination_since_id
- results.first.id
+ def pagination_collection
+ results
end
def records_continue?
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb
index 6a3567e624..a95c816e1c 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/conversations_controller.rb
@@ -53,10 +53,6 @@ class Api::V1::ConversationsController < Api::BaseController
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_conversations_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb
index 68cf4384f7..d3de220393 100644
--- a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb
+++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb
@@ -29,10 +29,6 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -41,12 +37,8 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
- def pagination_max_id
- @encrypted_messages.last.id
- end
-
- def pagination_since_id
- @encrypted_messages.first.id
+ def pagination_collection
+ @encrypted_messages
end
def records_continue?
diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb
index 34def3c44a..3dee2d176c 100644
--- a/app/controllers/api/v1/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/domain_blocks_controller.rb
@@ -38,10 +38,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
current_account.domain_blocks
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -50,12 +46,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
end
- def pagination_max_id
- @blocks.last.id
- end
-
- def pagination_since_id
- @blocks.first.id
+ def pagination_collection
+ @blocks
end
def records_continue?
diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb
index 2216a9860d..9a723d89e4 100644
--- a/app/controllers/api/v1/endorsements_controller.rb
+++ b/app/controllers/api/v1/endorsements_controller.rb
@@ -28,10 +28,6 @@ class Api::V1::EndorsementsController < Api::BaseController
current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
return if unlimited?
@@ -44,12 +40,8 @@ class Api::V1::EndorsementsController < Api::BaseController
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
- def pagination_max_id
- @accounts.last.id
- end
-
- def pagination_since_id
- @accounts.first.id
+ def pagination_collection
+ @accounts
end
def records_continue?
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index faf1bda96a..73da538f5c 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -31,10 +31,6 @@ class Api::V1::FavouritesController < Api::BaseController
current_account.favourites
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_favourites_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -43,12 +39,8 @@ class Api::V1::FavouritesController < Api::BaseController
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
- def pagination_max_id
- results.last.id
- end
-
- def pagination_since_id
- results.first.id
+ def pagination_collection
+ results
end
def records_continue?
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index 87f6df5f94..7ffd7614bb 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -48,10 +48,6 @@ class Api::V1::FollowRequestsController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/followed_tags_controller.rb b/app/controllers/api/v1/followed_tags_controller.rb
index eae2bdc010..8888612b16 100644
--- a/app/controllers/api/v1/followed_tags_controller.rb
+++ b/app/controllers/api/v1/followed_tags_controller.rb
@@ -22,10 +22,6 @@ class Api::V1::FollowedTagsController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_followed_tags_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -34,12 +30,8 @@ class Api::V1::FollowedTagsController < Api::BaseController
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
end
- def pagination_max_id
- @results.last.id
- end
-
- def pagination_since_id
- @results.first.id
+ def pagination_collection
+ @results
end
def records_continue?
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
index 0604ad60fc..aecf391049 100644
--- a/app/controllers/api/v1/lists/accounts_controller.rb
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -55,10 +55,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
params.permit(account_ids: [])
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
return if unlimited?
@@ -71,12 +67,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
- def pagination_max_id
- @accounts.last.id
- end
-
- def pagination_since_id
- @accounts.first.id
+ def pagination_collection
+ @accounts
end
def records_continue?
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index 2fb685ac39..dbfd7e103a 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -28,10 +28,6 @@ class Api::V1::MutesController < Api::BaseController
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_mutes_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -40,12 +36,8 @@ class Api::V1::MutesController < Api::BaseController
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
end
- def pagination_max_id
- paginated_mutes.last.id
- end
-
- def pagination_since_id
- paginated_mutes.first.id
+ def pagination_collection
+ paginated_mutes
end
def records_continue?
diff --git a/app/controllers/api/v1/notifications/policies_controller.rb b/app/controllers/api/v1/notifications/policies_controller.rb
new file mode 100644
index 0000000000..1ec336f9a5
--- /dev/null
+++ b/app/controllers/api/v1/notifications/policies_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class Api::V1::Notifications::PoliciesController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
+ before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
+
+ before_action :require_user!
+ before_action :set_policy
+
+ def show
+ render json: @policy, serializer: REST::NotificationPolicySerializer
+ end
+
+ def update
+ @policy.update!(resource_params)
+ render json: @policy, serializer: REST::NotificationPolicySerializer
+ end
+
+ private
+
+ def set_policy
+ @policy = NotificationPolicy.find_or_initialize_by(account: current_account)
+
+ with_read_replica do
+ @policy.summarize!
+ end
+ end
+
+ def resource_params
+ params.permit(
+ :filter_not_following,
+ :filter_not_followers,
+ :filter_new_accounts,
+ :filter_private_mentions
+ )
+ end
+end
diff --git a/app/controllers/api/v1/notifications/requests_controller.rb b/app/controllers/api/v1/notifications/requests_controller.rb
new file mode 100644
index 0000000000..6a26cc0e8a
--- /dev/null
+++ b/app/controllers/api/v1/notifications/requests_controller.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+class Api::V1::Notifications::RequestsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index
+ before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
+
+ before_action :require_user!
+ before_action :set_request, except: :index
+
+ after_action :insert_pagination_headers, only: :index
+
+ def index
+ with_read_replica do
+ @requests = load_requests
+ @relationships = relationships
+ end
+
+ render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
+ end
+
+ def show
+ render json: @request, serializer: REST::NotificationRequestSerializer
+ end
+
+ def accept
+ AcceptNotificationRequestService.new.call(@request)
+ render_empty
+ end
+
+ def dismiss
+ @request.update!(dismissed: true)
+ render_empty
+ end
+
+ private
+
+ def load_requests
+ requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
+ limit_param(DEFAULT_ACCOUNTS_LIMIT),
+ params_slice(:max_id, :since_id, :min_id)
+ )
+
+ NotificationRequest.preload_cache_collection(requests) do |statuses|
+ cache_collection(statuses, Status)
+ end
+ end
+
+ def relationships
+ StatusRelationshipsPresenter.new(@requests.map(&:last_status), current_user&.account_id)
+ end
+
+ def set_request
+ @request = NotificationRequest.where(account: current_account).find(params[:id])
+ end
+
+ def next_path
+ api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
+ end
+
+ def prev_path
+ api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
+ end
+
+ def pagination_max_id
+ @requests.last.id
+ end
+
+ def pagination_since_id
+ @requests.first.id
+ end
+
+ def pagination_params(core_params)
+ params.slice(:dismissed).permit(:dismissed).merge(core_params)
+ end
+end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index b1814e16ab..9c75516159 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -58,7 +58,8 @@ class Api::V1::NotificationsController < Api::BaseController
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),
exclude_types: Array(browserable_params[:exclude_types]),
- from_account_id: browserable_params[:account_id]
+ from_account_id: browserable_params[:account_id],
+ include_filtered: truthy_param?(:include_filtered)
)
end
@@ -66,10 +67,6 @@ class Api::V1::NotificationsController < Api::BaseController
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
end
@@ -78,19 +75,15 @@ class Api::V1::NotificationsController < Api::BaseController
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end
- def pagination_max_id
- @notifications.last.id
- end
-
- def pagination_since_id
- @notifications.first.id
+ def pagination_collection
+ @notifications
end
def browserable_params
- params.permit(:account_id, types: [], exclude_types: [])
+ params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
end
def pagination_params(core_params)
- params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params)
+ params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
end
end
diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb
index 2220b6d22e..1217ed014e 100644
--- a/app/controllers/api/v1/scheduled_statuses_controller.rb
+++ b/app/controllers/api/v1/scheduled_statuses_controller.rb
@@ -47,10 +47,6 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
params.slice(:limit).permit(:limit).merge(core_params)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@@ -63,11 +59,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
- def pagination_max_id
- @statuses.last.id
- end
-
- def pagination_since_id
- @statuses.first.id
+ def pagination_collection
+ @statuses
end
end
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index 069ad37cb2..bbc8082e0c 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -34,10 +34,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index b8a997518d..eaa5ef7255 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -30,10 +30,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
)
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def next_path
api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end
diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb
index 173e173cc9..e79eba79ee 100644
--- a/app/controllers/api/v1/timelines/base_controller.rb
+++ b/app/controllers/api/v1/timelines/base_controller.rb
@@ -5,16 +5,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
private
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
- def pagination_max_id
- @statuses.last.id
- end
-
- def pagination_since_id
- @statuses.first.id
+ def pagination_collection
+ @statuses
end
def next_path_params
diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb
index 57cfa0b7e4..8edf5bbcef 100644
--- a/app/controllers/api/v1/trends/links_controller.rb
+++ b/app/controllers/api/v1/trends/links_controller.rb
@@ -34,10 +34,6 @@ class Api::V1::Trends::LinksController < Api::BaseController
scope
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb
index c186864c3b..48bfe11991 100644
--- a/app/controllers/api/v1/trends/statuses_controller.rb
+++ b/app/controllers/api/v1/trends/statuses_controller.rb
@@ -32,10 +32,6 @@ class Api::V1::Trends::StatusesController < Api::BaseController
scope
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb
index 6cc8194def..0d8e27552e 100644
--- a/app/controllers/api/v1/trends/tags_controller.rb
+++ b/app/controllers/api/v1/trends/tags_controller.rb
@@ -30,10 +30,6 @@ class Api::V1::Trends::TagsController < Api::BaseController
Trends.tags.query.allowed
end
- def insert_pagination_headers
- set_pagination_headers(next_path, prev_path)
- end
-
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 490a45e018..c76110dba7 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
end
def single_user_mode?
- @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
+ @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
end
def use_seamless_external_login?
diff --git a/app/controllers/concerns/api/error_handling.rb b/app/controllers/concerns/api/error_handling.rb
new file mode 100644
index 0000000000..ad559fe2d7
--- /dev/null
+++ b/app/controllers/concerns/api/error_handling.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Api::ErrorHandling
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
+ render json: { error: e.to_s }, status: 422
+ end
+
+ rescue_from ActiveRecord::RecordNotUnique do
+ render json: { error: 'Duplicate record' }, status: 422
+ end
+
+ rescue_from Date::Error do
+ render json: { error: 'Invalid date supplied' }, status: 422
+ end
+
+ rescue_from ActiveRecord::RecordNotFound do
+ render json: { error: 'Record not found' }, status: 404
+ end
+
+ rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
+ render json: { error: 'Remote data could not be fetched' }, status: 503
+ end
+
+ rescue_from OpenSSL::SSL::SSLError do
+ render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
+ end
+
+ rescue_from Mastodon::NotPermittedError do
+ render json: { error: 'This action is not allowed' }, status: 403
+ end
+
+ rescue_from Seahorse::Client::NetworkingError do |e|
+ Rails.logger.warn "Storage server error: #{e}"
+ render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
+ end
+
+ rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
+ render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
+ end
+
+ rescue_from Mastodon::RateLimitExceededError do
+ render json: { error: I18n.t('errors.429') }, status: 429
+ end
+
+ rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
+ render json: { error: e.to_s }, status: 400
+ end
+ end
+end
diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb
index 8422d74bc3..f2b1eaa3e7 100644
--- a/app/controllers/instance_actors_controller.rb
+++ b/app/controllers/instance_actors_controller.rb
@@ -6,6 +6,8 @@ class InstanceActorsController < ActivityPub::BaseController
serialization_scope nil
before_action :set_account
+
+ skip_before_action :authenticate_user! # From `AccountOwnedConcern`
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
@@ -16,6 +18,11 @@ class InstanceActorsController < ActivityPub::BaseController
private
+ # Skips various `before_action` from `AccountOwnedConcern`
+ def account_required?
+ false
+ end
+
def set_account
@account = Account.representative
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index daa27195e9..233c242e4a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -28,14 +28,6 @@ module ApplicationHelper
number_to_human(number, **options)
end
- def active_nav_class(*paths)
- paths.any? { |path| current_page?(path) } ? 'active' : ''
- end
-
- def show_landing_strip?
- !user_signed_in? && !single_user_mode?
- end
-
def open_registrations?
Setting.registrations_mode == 'open'
end
@@ -122,7 +114,7 @@ module ApplicationHelper
end
def check_icon
- content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor')
+ inline_svg_tag 'check.svg'
end
def visibility_icon(status)
@@ -214,7 +206,7 @@ module ApplicationHelper
state_params[:moved_to_account] = current_account.moved_to_account
end
- state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode?
+ state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety
diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb
index 2b9c233c23..f72d6df5d9 100644
--- a/app/helpers/branding_helper.rb
+++ b/app/helpers/branding_helper.rb
@@ -21,15 +21,4 @@ module BrandingHelper
def render_logo
image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon')
end
-
- def render_symbol(version = :icon)
- path = case version
- when :icon
- 'logo-symbol-icon.svg'
- when :wordmark
- 'logo-symbol-wordmark.svg'
- end
-
- render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety
- end
end
diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb
index 1b79a089bc..412be4f48d 100644
--- a/app/helpers/context_helper.rb
+++ b/app/helpers/context_helper.rb
@@ -25,12 +25,21 @@ module ContextHelper
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: {
- 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId',
+ 'toot' => 'http://joinmastodon.org/ns#',
+ 'Device' => 'toot:Device',
+ 'Ed25519Signature' => 'toot:Ed25519Signature',
+ 'Ed25519Key' => 'toot:Ed25519Key',
+ 'Curve25519Key' => 'toot:Curve25519Key',
+ 'EncryptedMessage' => 'toot:EncryptedMessage',
+ 'publicKeyBase64' => 'toot:publicKeyBase64',
+ 'deviceId' => 'toot:deviceId',
'claim' => { '@type' => '@id', '@id' => 'toot:claim' },
'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' },
'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' },
'devices' => { '@type' => '@id', '@id' => 'toot:devices' },
- 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText'
+ 'messageFranking' => 'toot:messageFranking',
+ 'messageType' => 'toot:messageType',
+ 'cipherText' => 'toot:cipherText',
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze
diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb
index 87f0f288d3..9e1c0a7db1 100644
--- a/app/helpers/languages_helper.rb
+++ b/app/helpers/languages_helper.rb
@@ -109,6 +109,7 @@ module LanguagesHelper
mn: ['Mongolian', 'Монгол хэл'].freeze,
mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Melayu'].freeze,
+ 'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
mt: ['Maltese', 'Malti'].freeze,
my: ['Burmese', 'ဗမာစာ'].freeze,
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
@@ -127,7 +128,7 @@ module LanguagesHelper
om: ['Oromo', 'Afaan Oromoo'].freeze,
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
os: ['Ossetian', 'ирон æвзаг'].freeze,
- pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
+ pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze,
pi: ['Pāli', 'पाऴि'].freeze,
pl: ['Polish', 'Polski'].freeze,
ps: ['Pashto', 'پښتو'].freeze,
@@ -191,15 +192,20 @@ module LanguagesHelper
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze,
+ csb: ['Kashubian', 'Kaszëbsczi'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze,
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
+ moh: ['Mohawk', 'Kanienʼkéha'].freeze,
+ nds: ['Low German', 'Plattdüütsch'].freeze,
+ pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
sco: ['Scots', 'Scots'].freeze,
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze,
+ vai: ['Vai', 'ꕙꔤ'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index 286c53d834..ca693a8a78 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -4,14 +4,6 @@ module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'
- def link_to_newer(url)
- link_to t('statuses.show_newer'), url, class: 'load-more load-gap'
- end
-
- def link_to_older(url)
- link_to t('statuses.show_older'), url, class: 'load-more load-gap'
- end
-
def nothing_here(extra_classes = '')
content_tag(:div, class: "nothing-here #{extra_classes}") do
t('accounts.nothing_here')
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
deleted file mode 100644
index 655133290a..0000000000
--- a/app/javascript/core/admin.js
+++ /dev/null
@@ -1,220 +0,0 @@
-// This file will be loaded on admin pages, regardless of theme.
-
-import 'packs/public-path';
-import Rails from '@rails/ujs';
-
-import ready from '../mastodon/ready';
-
-const setAnnouncementEndsAttributes = (target) => {
- const valid = target?.value && target?.validity?.valid;
- const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
- if (valid) {
- element.classList.remove('optional');
- element.required = true;
- element.min = target.value;
- } else {
- element.classList.add('optional');
- element.removeAttribute('required');
- element.removeAttribute('min');
- }
-};
-
-Rails.delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
- setAnnouncementEndsAttributes(target);
-});
-
-const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
-
-const showSelectAll = () => {
- const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
- selectAllMatchingElement.classList.add('active');
-};
-
-const hideSelectAll = () => {
- const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
- const hiddenField = document.querySelector('#select_all_matching');
- const selectedMsg = document.querySelector('.batch-table__select-all .selected');
- const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
-
- selectAllMatchingElement.classList.remove('active');
- selectedMsg.classList.remove('active');
- notSelectedMsg.classList.add('active');
- hiddenField.value = '0';
-};
-
-Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
- const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
-
- [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
- content.checked = target.checked;
- });
-
- if (selectAllMatchingElement) {
- if (target.checked) {
- showSelectAll();
- } else {
- hideSelectAll();
- }
- }
-});
-
-Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
- const hiddenField = document.querySelector('#select_all_matching');
- const active = hiddenField.value === '1';
- const selectedMsg = document.querySelector('.batch-table__select-all .selected');
- const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
-
- if (active) {
- hiddenField.value = '0';
- selectedMsg.classList.remove('active');
- notSelectedMsg.classList.add('active');
- } else {
- hiddenField.value = '1';
- notSelectedMsg.classList.remove('active');
- selectedMsg.classList.add('active');
- }
-});
-
-Rails.delegate(document, batchCheckboxClassName, 'change', () => {
- const checkAllElement = document.querySelector('#batch_checkbox_all');
- const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
-
- if (checkAllElement) {
- checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
- checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
-
- if (selectAllMatchingElement) {
- if (checkAllElement.checked) {
- showSelectAll();
- } else {
- hideSelectAll();
- }
- }
- }
-});
-
-Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
- target.form.submit();
-});
-
-const onDomainBlockSeverityChange = (target) => {
- const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
- const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
-
- if (rejectMediaDiv) {
- rejectMediaDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
- }
-
- if (rejectReportsDiv) {
- rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
- }
-};
-
-Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
-
-const onEnableBootstrapTimelineAccountsChange = (target) => {
- const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
-
- if (bootstrapTimelineAccountsField) {
- bootstrapTimelineAccountsField.disabled = !target.checked;
- if (target.checked) {
- bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
- bootstrapTimelineAccountsField.parentElement.parentElement.classList.remove('disabled');
- } else {
- bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
- bootstrapTimelineAccountsField.parentElement.parentElement.classList.add('disabled');
- }
- }
-};
-
-Rails.delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
-
-const onChangeRegistrationMode = (target) => {
- const enabled = target.value === 'approved';
-
- [].forEach.call(document.querySelectorAll('.form_admin_settings_registrations_mode .warning-hint'), (warning_hint) => {
- warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
- });
-
- [].forEach.call(document.querySelectorAll('#form_admin_settings_require_invite_text'), (input) => {
- input.disabled = !enabled;
- if (enabled) {
- let element = input;
- do {
- element.classList.remove('disabled');
- element = element.parentElement;
- } while (element && !element.classList.contains('fields-group'));
- } else {
- let element = input;
- do {
- element.classList.add('disabled');
- element = element.parentElement;
- } while (element && !element.classList.contains('fields-group'));
- }
- });
-};
-
-const convertUTCDateTimeToLocal = (value) => {
- const date = new Date(value + 'Z');
- const twoChars = (x) => (x.toString().padStart(2, '0'));
- return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
-};
-
-const convertLocalDatetimeToUTC = (value) => {
- const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
- const match = re.exec(value);
- const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
- const fullISO8601 = date.toISOString();
- return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
-};
-
-Rails.delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
-
-ready(() => {
- const domainBlockSeverityInput = document.getElementById('domain_block_severity');
- if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
-
- const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
- if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
-
- const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
- if (registrationMode) onChangeRegistrationMode(registrationMode);
-
- const checkAllElement = document.querySelector('#batch_checkbox_all');
- if (checkAllElement) {
- checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
- checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
- }
-
- document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => {
- const domain = document.querySelector('input[type="text"]#by_domain')?.value;
-
- if (domain) {
- const url = new URL(event.target.href);
- url.searchParams.set('_domain', domain);
- e.target.href = url;
- }
- });
-
- [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => {
- if (element.value) {
- element.value = convertUTCDateTimeToLocal(element.value);
- }
- if (element.placeholder) {
- element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
- }
- });
-
- Rails.delegate(document, 'form', 'submit', ({ target }) => {
- [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
- if (element.value && element.validity.valid) {
- element.value = convertLocalDatetimeToUTC(element.value);
- }
- });
- });
-
- const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
- if (announcementStartsAt) {
- setAnnouncementEndsAttributes(announcementStartsAt);
- }
-});
diff --git a/app/javascript/core/admin.ts b/app/javascript/core/admin.ts
new file mode 100644
index 0000000000..0642affef0
--- /dev/null
+++ b/app/javascript/core/admin.ts
@@ -0,0 +1,340 @@
+// This file will be loaded on admin pages, regardless of theme.
+
+import 'packs/public-path';
+
+import Rails from '@rails/ujs';
+
+import ready from '../mastodon/ready';
+
+const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
+ const valid = target.value && target.validity.valid;
+ const element = document.querySelector(
+ 'input[type="datetime-local"]#announcement_ends_at',
+ );
+
+ if (!element) return;
+
+ if (valid) {
+ element.classList.remove('optional');
+ element.required = true;
+ element.min = target.value;
+ } else {
+ element.classList.add('optional');
+ element.removeAttribute('required');
+ element.removeAttribute('min');
+ }
+};
+
+Rails.delegate(
+ document,
+ 'input[type="datetime-local"]#announcement_starts_at',
+ 'change',
+ ({ target }) => {
+ if (target instanceof HTMLInputElement)
+ setAnnouncementEndsAttributes(target);
+ },
+);
+
+const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
+
+const showSelectAll = () => {
+ const selectAllMatchingElement = document.querySelector(
+ '.batch-table__select-all',
+ );
+ selectAllMatchingElement?.classList.add('active');
+};
+
+const hideSelectAll = () => {
+ const selectAllMatchingElement = document.querySelector(
+ '.batch-table__select-all',
+ );
+ const hiddenField = document.querySelector(
+ 'input#select_all_matching',
+ );
+ const selectedMsg = document.querySelector(
+ '.batch-table__select-all .selected',
+ );
+ const notSelectedMsg = document.querySelector(
+ '.batch-table__select-all .not-selected',
+ );
+
+ selectAllMatchingElement?.classList.remove('active');
+ selectedMsg?.classList.remove('active');
+ notSelectedMsg?.classList.add('active');
+ if (hiddenField) hiddenField.value = '0';
+};
+
+Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
+ if (!(target instanceof HTMLInputElement)) return;
+
+ const selectAllMatchingElement = document.querySelector(
+ '.batch-table__select-all',
+ );
+
+ document
+ .querySelectorAll(batchCheckboxClassName)
+ .forEach((content) => {
+ content.checked = target.checked;
+ });
+
+ if (selectAllMatchingElement) {
+ if (target.checked) {
+ showSelectAll();
+ } else {
+ hideSelectAll();
+ }
+ }
+});
+
+Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
+ const hiddenField = document.querySelector(
+ '#select_all_matching',
+ );
+
+ if (!hiddenField) return;
+
+ const active = hiddenField.value === '1';
+ const selectedMsg = document.querySelector(
+ '.batch-table__select-all .selected',
+ );
+ const notSelectedMsg = document.querySelector(
+ '.batch-table__select-all .not-selected',
+ );
+
+ if (!selectedMsg || !notSelectedMsg) return;
+
+ if (active) {
+ hiddenField.value = '0';
+ selectedMsg.classList.remove('active');
+ notSelectedMsg.classList.add('active');
+ } else {
+ hiddenField.value = '1';
+ notSelectedMsg.classList.remove('active');
+ selectedMsg.classList.add('active');
+ }
+});
+
+Rails.delegate(document, batchCheckboxClassName, 'change', () => {
+ const checkAllElement = document.querySelector(
+ 'input#batch_checkbox_all',
+ );
+ const selectAllMatchingElement = document.querySelector(
+ '.batch-table__select-all',
+ );
+
+ if (checkAllElement) {
+ const allCheckboxes = Array.from(
+ document.querySelectorAll(batchCheckboxClassName),
+ );
+ checkAllElement.checked = allCheckboxes.every((content) => content.checked);
+ checkAllElement.indeterminate =
+ !checkAllElement.checked &&
+ allCheckboxes.some((content) => content.checked);
+
+ if (selectAllMatchingElement) {
+ if (checkAllElement.checked) {
+ showSelectAll();
+ } else {
+ hideSelectAll();
+ }
+ }
+ }
+});
+
+Rails.delegate(
+ document,
+ '.filter-subset--with-select select',
+ 'change',
+ ({ target }) => {
+ if (target instanceof HTMLSelectElement) target.form?.submit();
+ },
+);
+
+const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
+ const rejectMediaDiv = document.querySelector(
+ '.input.with_label.domain_block_reject_media',
+ );
+ const rejectReportsDiv = document.querySelector(
+ '.input.with_label.domain_block_reject_reports',
+ );
+
+ if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
+ rejectMediaDiv.style.display =
+ target.value === 'suspend' ? 'none' : 'block';
+ }
+
+ if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
+ rejectReportsDiv.style.display =
+ target.value === 'suspend' ? 'none' : 'block';
+ }
+};
+
+Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
+ if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
+});
+
+const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
+ const bootstrapTimelineAccountsField =
+ document.querySelector(
+ '#form_admin_settings_bootstrap_timeline_accounts',
+ );
+
+ if (bootstrapTimelineAccountsField) {
+ bootstrapTimelineAccountsField.disabled = !target.checked;
+ if (target.checked) {
+ bootstrapTimelineAccountsField.parentElement?.classList.remove(
+ 'disabled',
+ );
+ bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
+ 'disabled',
+ );
+ } else {
+ bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
+ bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
+ 'disabled',
+ );
+ }
+ }
+};
+
+Rails.delegate(
+ document,
+ '#form_admin_settings_enable_bootstrap_timeline_accounts',
+ 'change',
+ ({ target }) => {
+ if (target instanceof HTMLInputElement)
+ onEnableBootstrapTimelineAccountsChange(target);
+ },
+);
+
+const onChangeRegistrationMode = (target: HTMLSelectElement) => {
+ const enabled = target.value === 'approved';
+
+ document
+ .querySelectorAll(
+ '.form_admin_settings_registrations_mode .warning-hint',
+ )
+ .forEach((warning_hint) => {
+ warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
+ });
+
+ document
+ .querySelectorAll(
+ 'input#form_admin_settings_require_invite_text',
+ )
+ .forEach((input) => {
+ input.disabled = !enabled;
+ if (enabled) {
+ let element: HTMLElement | null = input;
+ do {
+ element.classList.remove('disabled');
+ element = element.parentElement;
+ } while (element && !element.classList.contains('fields-group'));
+ } else {
+ let element: HTMLElement | null = input;
+ do {
+ element.classList.add('disabled');
+ element = element.parentElement;
+ } while (element && !element.classList.contains('fields-group'));
+ }
+ });
+};
+
+const convertUTCDateTimeToLocal = (value: string) => {
+ const date = new Date(value + 'Z');
+ const twoChars = (x: number) => x.toString().padStart(2, '0');
+ return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
+};
+
+function convertLocalDatetimeToUTC(value: string) {
+ const date = new Date(value);
+ const fullISO8601 = date.toISOString();
+ return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
+}
+
+Rails.delegate(
+ document,
+ '#form_admin_settings_registrations_mode',
+ 'change',
+ ({ target }) => {
+ if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
+ },
+);
+
+ready(() => {
+ const domainBlockSeveritySelect = document.querySelector(
+ 'select#domain_block_severity',
+ );
+ if (domainBlockSeveritySelect)
+ onDomainBlockSeverityChange(domainBlockSeveritySelect);
+
+ const enableBootstrapTimelineAccounts =
+ document.querySelector(
+ 'input#form_admin_settings_enable_bootstrap_timeline_accounts',
+ );
+ if (enableBootstrapTimelineAccounts)
+ onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
+
+ const registrationMode = document.querySelector(
+ 'select#form_admin_settings_registrations_mode',
+ );
+ if (registrationMode) onChangeRegistrationMode(registrationMode);
+
+ const checkAllElement = document.querySelector(
+ 'input#batch_checkbox_all',
+ );
+ if (checkAllElement) {
+ const allCheckboxes = Array.from(
+ document.querySelectorAll(batchCheckboxClassName),
+ );
+ checkAllElement.checked = allCheckboxes.every((content) => content.checked);
+ checkAllElement.indeterminate =
+ !checkAllElement.checked &&
+ allCheckboxes.some((content) => content.checked);
+ }
+
+ document
+ .querySelector('a#add-instance-button')
+ ?.addEventListener('click', (e) => {
+ const domain = document.querySelector(
+ 'input[type="text"]#by_domain',
+ )?.value;
+
+ if (domain && e.target instanceof HTMLAnchorElement) {
+ const url = new URL(e.target.href);
+ url.searchParams.set('_domain', domain);
+ e.target.href = url.toString();
+ }
+ });
+
+ document
+ .querySelectorAll('input[type="datetime-local"]')
+ .forEach((element) => {
+ if (element.value) {
+ element.value = convertUTCDateTimeToLocal(element.value);
+ }
+ if (element.placeholder) {
+ element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
+ }
+ });
+
+ Rails.delegate(document, 'form', 'submit', ({ target }) => {
+ if (target instanceof HTMLFormElement)
+ target
+ .querySelectorAll('input[type="datetime-local"]')
+ .forEach((element) => {
+ if (element.value && element.validity.valid) {
+ element.value = convertLocalDatetimeToUTC(element.value);
+ }
+ });
+ });
+
+ const announcementStartsAt = document.querySelector(
+ 'input[type="datetime-local"]#announcement_starts_at',
+ );
+ if (announcementStartsAt) {
+ setAnnouncementEndsAttributes(announcementStartsAt);
+ }
+}).catch((reason) => {
+ throw reason;
+});
diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js
deleted file mode 100644
index d1e8f6b108..0000000000
--- a/app/javascript/core/embed.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// This file will be loaded on embed pages, regardless of theme.
-
-import 'packs/public-path';
-
-window.addEventListener('message', e => {
- const data = e.data || {};
-
- if (!window.parent || data.type !== 'setHeight') {
- return;
- }
-
- function setEmbedHeight () {
- window.parent.postMessage({
- type: 'setHeight',
- id: data.id,
- height: document.getElementsByTagName('html')[0].scrollHeight,
- }, '*');
- }
-
- if (['interactive', 'complete'].includes(document.readyState)) {
- setEmbedHeight();
- } else {
- document.addEventListener('DOMContentLoaded', setEmbedHeight);
- }
-});
diff --git a/app/javascript/core/embed.ts b/app/javascript/core/embed.ts
new file mode 100644
index 0000000000..6766cd7788
--- /dev/null
+++ b/app/javascript/core/embed.ts
@@ -0,0 +1,41 @@
+// This file will be loaded on embed pages, regardless of theme.
+
+import 'packs/public-path';
+import ready from '../mastodon/ready';
+
+interface SetHeightMessage {
+ type: 'setHeight';
+ id: string;
+ height: number;
+}
+
+function isSetHeightMessage(data: unknown): data is SetHeightMessage {
+ if (
+ data &&
+ typeof data === 'object' &&
+ 'type' in data &&
+ data.type === 'setHeight'
+ )
+ return true;
+ else return false;
+}
+
+window.addEventListener('message', (e) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
+ if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
+
+ const data = e.data;
+
+ ready(() => {
+ window.parent.postMessage(
+ {
+ type: 'setHeight',
+ id: data.id,
+ height: document.getElementsByTagName('html')[0].scrollHeight,
+ },
+ '*',
+ );
+ }).catch((e) => {
+ console.error('Error in setHeightMessage postMessage', e);
+ });
+});
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
deleted file mode 100644
index 23367d2d31..0000000000
--- a/app/javascript/core/settings.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// This file will be loaded on settings pages, regardless of theme.
-
-import 'packs/public-path';
-import Rails from '@rails/ujs';
-
-Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
- const avatar = document.getElementById(target.id + '-preview');
- const [file] = target.files || [];
- const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
-
- avatar.src = url;
-});
-
-Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
- target.focus();
- target.select();
- target.setSelectionRange(0, target.value.length);
-});
-
-Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
- const input = target.parentNode.querySelector('.input-copy__wrapper input');
-
- const oldReadOnly = input.readonly;
-
- input.readonly = false;
- input.focus();
- input.select();
- input.setSelectionRange(0, input.value.length);
-
- try {
- if (document.execCommand('copy')) {
- input.blur();
- target.parentNode.classList.add('copied');
-
- setTimeout(() => {
- target.parentNode.classList.remove('copied');
- }, 700);
- }
- } catch (err) {
- console.error(err);
- }
-
- input.readonly = oldReadOnly;
-});
diff --git a/app/javascript/core/settings.ts b/app/javascript/core/settings.ts
new file mode 100644
index 0000000000..ea6a99ec80
--- /dev/null
+++ b/app/javascript/core/settings.ts
@@ -0,0 +1,70 @@
+// This file will be loaded on settings pages, regardless of theme.
+
+import 'packs/public-path';
+import Rails from '@rails/ujs';
+
+Rails.delegate(
+ document,
+ '#edit_profile input[type=file]',
+ 'change',
+ ({ target }) => {
+ if (!(target instanceof HTMLInputElement)) return;
+
+ const avatar = document.querySelector(
+ `img#${target.id}-preview`,
+ );
+
+ if (!avatar) return;
+
+ let file: File | undefined;
+ if (target.files) file = target.files[0];
+
+ const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
+
+ if (url) avatar.src = url;
+ },
+);
+
+Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
+ if (!(target instanceof HTMLInputElement)) return;
+
+ target.focus();
+ target.select();
+ target.setSelectionRange(0, target.value.length);
+});
+
+Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
+ if (!(target instanceof HTMLButtonElement)) return;
+
+ const input = target.parentNode?.querySelector(
+ '.input-copy__wrapper input',
+ );
+
+ if (!input) return;
+
+ const oldReadOnly = input.readOnly;
+
+ input.readOnly = false;
+ input.focus();
+ input.select();
+ input.setSelectionRange(0, input.value.length);
+
+ try {
+ if (document.execCommand('copy')) {
+ input.blur();
+
+ const parent = target.parentElement;
+
+ if (!parent) return;
+ parent.classList.add('copied');
+
+ setTimeout(() => {
+ parent.classList.remove('copied');
+ }, 700);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ input.readOnly = oldReadOnly;
+});
diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml
index f6f653c0a9..12c23e2035 100644
--- a/app/javascript/core/theme.yml
+++ b/app/javascript/core/theme.yml
@@ -2,12 +2,12 @@
# theme.
pack:
about:
- admin: admin.js
+ admin: admin.ts
auth: auth.js
common:
filename: common.js
stylesheet: true
- embed: embed.js
+ embed: embed.ts
error:
home:
inert:
@@ -18,7 +18,7 @@ pack:
stylesheet: true
modal:
public:
- settings: settings.js
+ settings: settings.ts
sign_up:
share:
remote_interaction_helper: remote_interaction_helper.ts
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index af10af9fe9..bb26035e97 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -66,11 +66,9 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
-export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
-export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
@@ -93,11 +91,6 @@ export * from './accounts_typed';
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
-
- if (getState().getIn(['accounts', id], null) !== null) {
- return;
- }
-
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js
index e293657ad3..54296d0905 100644
--- a/app/javascript/flavours/glitch/actions/blocks.js
+++ b/app/javascript/flavours/glitch/actions/blocks.js
@@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
-export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
-
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
@@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) {
return dispatch => {
- dispatch({
- type: BLOCKS_INIT_MODAL,
- account,
- });
-
- dispatch(openModal({ modalType: 'BLOCK' }));
+ dispatch(openModal({
+ modalType: 'BLOCK',
+ modalProps: {
+ accountId: account.get('id'),
+ acct: account.get('acct'),
+ },
+ }));
};
}
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 06f0d79874..2646aa7b02 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -266,12 +266,14 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
insertIfOnline('direct');
}
- dispatch(showAlert({
- message: statusId === null ? messages.published : messages.saved,
- action: messages.open,
- dismissAfter: 10000,
- onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
- }));
+ if (getState().getIn(['local_settings', 'show_published_toast'])) {
+ dispatch(showAlert({
+ message: statusId === null ? messages.published : messages.saved,
+ action: messages.open,
+ dismissAfter: 10000,
+ onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
+ }));
+ }
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@@ -820,11 +822,12 @@ export function addPollOption(title) {
};
}
-export function changePollOption(index, title) {
+export function changePollOption(index, title, maxOptions) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
+ maxOptions,
};
}
diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js
index 718002613f..55c0a6ce9d 100644
--- a/app/javascript/flavours/glitch/actions/domain_blocks.js
+++ b/app/javascript/flavours/glitch/actions/domain_blocks.js
@@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
+import { openModal } from './modal';
+
export * from "./domain_blocks_typed";
@@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error,
};
}
+
+export const initDomainBlockModal = account => dispatch => dispatch(openModal({
+ modalType: 'DOMAIN_BLOCK',
+ modalProps: {
+ domain: account.get('acct').split('@')[1],
+ acct: account.get('acct'),
+ accountId: account.get('id'),
+ },
+}));
diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js
index fb041078b8..99c113f414 100644
--- a/app/javascript/flavours/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
-export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
-export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
-export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
-
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) {
return dispatch => {
- dispatch({
- type: MUTES_INIT_MODAL,
- account,
- });
-
- dispatch(openModal({ modalType: 'MUTE' }));
- };
-}
-
-export function toggleHideNotifications() {
- return dispatch => {
- dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
- };
-}
-
-export function changeMuteDuration(duration) {
- return dispatch => {
- dispatch({
- type: MUTES_CHANGE_DURATION,
- duration,
- });
+ dispatch(openModal({
+ modalType: 'MUTE',
+ modalProps: {
+ accountId: account.get('id'),
+ acct: account.get('acct'),
+ },
+ }));
};
}
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 6a0bf01b90..ff50e8f7d0 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -57,6 +57,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
+export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
+export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
+export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
+
+export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
+export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
+export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
+
+export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
+export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
+export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
+
+export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
+export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
+export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
+
+export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
+export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
+export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
+
+export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
+export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
+export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
+
+export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
+export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
+export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
+
+export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
+export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
+export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
+
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
@@ -402,3 +434,270 @@ export function setBrowserPermission (value) {
value,
};
}
+
+export const fetchNotificationPolicy = () => (dispatch, getState) => {
+ dispatch(fetchNotificationPolicyRequest());
+
+ api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
+ dispatch(fetchNotificationPolicySuccess(data));
+ }).catch(err => {
+ dispatch(fetchNotificationPolicyFail(err));
+ });
+};
+
+export const fetchNotificationPolicyRequest = () => ({
+ type: NOTIFICATION_POLICY_FETCH_REQUEST,
+});
+
+export const fetchNotificationPolicySuccess = policy => ({
+ type: NOTIFICATION_POLICY_FETCH_SUCCESS,
+ policy,
+});
+
+export const fetchNotificationPolicyFail = error => ({
+ type: NOTIFICATION_POLICY_FETCH_FAIL,
+ error,
+});
+
+export const updateNotificationsPolicy = params => (dispatch, getState) => {
+ dispatch(fetchNotificationPolicyRequest());
+
+ api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
+ dispatch(fetchNotificationPolicySuccess(data));
+ }).catch(err => {
+ dispatch(fetchNotificationPolicyFail(err));
+ });
+};
+
+export const fetchNotificationRequests = () => (dispatch, getState) => {
+ const params = {};
+
+ if (getState().getIn(['notificationRequests', 'isLoading'])) {
+ return;
+ }
+
+ if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
+ params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
+ }
+
+ dispatch(fetchNotificationRequestsRequest());
+
+ api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data.map(x => x.account)));
+ dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(fetchNotificationRequestsFail(err));
+ });
+};
+
+export const fetchNotificationRequestsRequest = () => ({
+ type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
+});
+
+export const fetchNotificationRequestsSuccess = (requests, next) => ({
+ type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
+ requests,
+ next,
+});
+
+export const fetchNotificationRequestsFail = error => ({
+ type: NOTIFICATION_REQUESTS_FETCH_FAIL,
+ error,
+});
+
+export const expandNotificationRequests = () => (dispatch, getState) => {
+ const url = getState().getIn(['notificationRequests', 'next']);
+
+ if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
+ return;
+ }
+
+ dispatch(expandNotificationRequestsRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data.map(x => x.account)));
+ dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
+ }).catch(err => {
+ dispatch(expandNotificationRequestsFail(err));
+ });
+};
+
+export const expandNotificationRequestsRequest = () => ({
+ type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
+});
+
+export const expandNotificationRequestsSuccess = (requests, next) => ({
+ type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
+ requests,
+ next,
+});
+
+export const expandNotificationRequestsFail = error => ({
+ type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
+ error,
+});
+
+export const fetchNotificationRequest = id => (dispatch, getState) => {
+ const current = getState().getIn(['notificationRequests', 'current']);
+
+ if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
+ return;
+ }
+
+ dispatch(fetchNotificationRequestRequest(id));
+
+ api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
+ dispatch(fetchNotificationRequestSuccess(data));
+ }).catch(err => {
+ dispatch(fetchNotificationRequestFail(id, err));
+ });
+};
+
+export const fetchNotificationRequestRequest = id => ({
+ type: NOTIFICATION_REQUEST_FETCH_REQUEST,
+ id,
+});
+
+export const fetchNotificationRequestSuccess = request => ({
+ type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
+ request,
+});
+
+export const fetchNotificationRequestFail = (id, error) => ({
+ type: NOTIFICATION_REQUEST_FETCH_FAIL,
+ id,
+ error,
+});
+
+export const acceptNotificationRequest = id => (dispatch, getState) => {
+ dispatch(acceptNotificationRequestRequest(id));
+
+ api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
+ dispatch(acceptNotificationRequestSuccess(id));
+ }).catch(err => {
+ dispatch(acceptNotificationRequestFail(id, err));
+ });
+};
+
+export const acceptNotificationRequestRequest = id => ({
+ type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
+ id,
+});
+
+export const acceptNotificationRequestSuccess = id => ({
+ type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
+ id,
+});
+
+export const acceptNotificationRequestFail = (id, error) => ({
+ type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
+ id,
+ error,
+});
+
+export const dismissNotificationRequest = id => (dispatch, getState) => {
+ dispatch(dismissNotificationRequestRequest(id));
+
+ api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
+ dispatch(dismissNotificationRequestSuccess(id));
+ }).catch(err => {
+ dispatch(dismissNotificationRequestFail(id, err));
+ });
+};
+
+export const dismissNotificationRequestRequest = id => ({
+ type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
+ id,
+});
+
+export const dismissNotificationRequestSuccess = id => ({
+ type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
+ id,
+});
+
+export const dismissNotificationRequestFail = (id, error) => ({
+ type: NOTIFICATION_REQUEST_DISMISS_FAIL,
+ id,
+ error,
+});
+
+export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
+ const current = getState().getIn(['notificationRequests', 'current']);
+ const params = { account_id: accountId };
+
+ if (current.getIn(['item', 'account']) === accountId) {
+ if (current.getIn(['notifications', 'isLoading'])) {
+ return;
+ }
+
+ if (current.getIn(['notifications', 'items'])?.size > 0) {
+ params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
+ }
+ }
+
+ dispatch(fetchNotificationsForRequestRequest());
+
+ api(getState).get('/api/v1/notifications', { params }).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data.map(item => item.account)));
+ dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+ dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
+
+ dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
+ }).catch(err => {
+ dispatch(fetchNotificationsForRequestFail(err));
+ });
+};
+
+export const fetchNotificationsForRequestRequest = () => ({
+ type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
+});
+
+export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
+ type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
+ notifications,
+ next,
+});
+
+export const fetchNotificationsForRequestFail = (error) => ({
+ type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
+ error,
+});
+
+export const expandNotificationsForRequest = () => (dispatch, getState) => {
+ const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
+
+ if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
+ return;
+ }
+
+ dispatch(expandNotificationsForRequestRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data.map(item => item.account)));
+ dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+ dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
+
+ dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
+ }).catch(err => {
+ dispatch(expandNotificationsForRequestFail(err));
+ });
+};
+
+export const expandNotificationsForRequestRequest = () => ({
+ type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
+});
+
+export const expandNotificationsForRequestSuccess = (notifications, next) => ({
+ type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
+ notifications,
+ next,
+});
+
+export const expandNotificationsForRequestFail = (error) => ({
+ type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
+ error,
+});
diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index 7e54740d52..44344e2fb5 100644
--- a/app/javascript/flavours/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -143,11 +143,14 @@ export const showSearch = () => ({
type: SEARCH_SHOW,
});
-export const openURL = routerHistory => (dispatch, getState) => {
- const value = getState().getIn(['search', 'value']);
+export const openURL = (value, history, onFailure) => (dispatch, getState) => {
const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) {
+ if (onFailure) {
+ onFailure();
+ }
+
return;
}
@@ -156,15 +159,21 @@ export const openURL = routerHistory => (dispatch, getState) => {
api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
- routerHistory.push(`/@${response.data.accounts[0].acct}`);
+ history.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
- routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+ history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+ } else if (onFailure) {
+ onFailure();
}
dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => {
dispatch(fetchSearchFail(err));
+
+ if (onFailure) {
+ onFailure();
+ }
});
};
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 5bdd31c343..332057ee67 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -1,7 +1,7 @@
import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
-import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { deleteFromTimelines } from './timelines';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@@ -138,10 +138,10 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
+ dispatch(importFetchedAccount(response.data.account));
if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type));
-
ensureComposeIsVisible(getState, routerHistory);
}
}).catch(error => {
diff --git a/app/javascript/flavours/glitch/components/attachment_list.jsx b/app/javascript/flavours/glitch/components/attachment_list.jsx
index 44b8bf78ee..60800f41ec 100644
--- a/app/javascript/flavours/glitch/components/attachment_list.jsx
+++ b/app/javascript/flavours/glitch/components/attachment_list.jsx
@@ -10,7 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
export default class AttachmentList extends ImmutablePureComponent {
diff --git a/app/javascript/flavours/glitch/components/check_box.tsx b/app/javascript/flavours/glitch/components/check_box.tsx
new file mode 100644
index 0000000000..7da8ef0ac5
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/check_box.tsx
@@ -0,0 +1,39 @@
+import classNames from 'classnames';
+
+import DoneIcon from '@/material-icons/400-24px/done.svg?react';
+
+import { Icon } from './icon';
+
+interface Props {
+ value: string;
+ checked: boolean;
+ name: string;
+ onChange: (event: React.ChangeEvent) => void;
+ label: React.ReactNode;
+}
+
+export const CheckBox: React.FC = ({
+ name,
+ value,
+ checked,
+ onChange,
+ label,
+}) => {
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx
index ec99698d0c..c2524b6dd9 100644
--- a/app/javascript/flavours/glitch/components/column_header.jsx
+++ b/app/javascript/flavours/glitch/components/column_header.jsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent, useCallback } from 'react';
-import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
@@ -11,12 +11,11 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
+import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
-
import { useAppHistory } from './router';
const messages = defineMessages({
@@ -24,10 +23,12 @@ const messages = defineMessages({
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
+ back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
-const BackButton = ({ pinned, show }) => {
+const BackButton = ({ pinned, show, onlyIcon }) => {
const history = useAppHistory();
+ const intl = useIntl();
const { multiColumn } = useColumnsContext();
const handleBackClick = useCallback(() => {
@@ -40,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
- if(!showButton) return null;
-
- return ();
+ if (!showButton) return null;
+ return (
+
+ );
};
BackButton.propTypes = {
pinned: PropTypes.bool,
show: PropTypes.bool,
+ onlyIcon: PropTypes.bool,
};
class ColumnHeader extends PureComponent {
@@ -146,27 +149,31 @@ class ColumnHeader extends PureComponent {
}
if (multiColumn && pinned) {
- pinButton = ;
+ pinButton = ;
moveButtons = (
-
+
);
} else if (multiColumn && this.props.onPin) {
- pinButton =
;
+ pinButton =
;
}
- backButton =
;
+ backButton =
;
const collapsedContent = [
extraContent,
];
if (multiColumn) {
- collapsedContent.push(pinButton);
- collapsedContent.push(moveButtons);
+ collapsedContent.push(
+
+ {pinButton}
+ {moveButtons}
+
+ );
}
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
@@ -178,7 +185,7 @@ class ColumnHeader extends PureComponent {
onClick={this.handleToggleClick}
>
-
+
{collapseIssues && }
@@ -191,16 +198,19 @@ class ColumnHeader extends PureComponent {
{hasTitle && (
-
+ <>
+ {showBackButton && backButton}
+
+
+ >
)}
- {!hasTitle && backButton}
+ {!hasTitle && showBackButton && backButton}
- {hasTitle && backButton}
{extraButton}
{collapseButton}
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx
index 4d9c34a762..26c828fd64 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx
@@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
-import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
}) : (
);
diff --git a/app/javascript/flavours/glitch/components/logo.jsx b/app/javascript/flavours/glitch/components/logo.tsx
similarity index 72%
rename from app/javascript/flavours/glitch/components/logo.jsx
rename to app/javascript/flavours/glitch/components/logo.tsx
index 73a94af9ed..b7f8bd6695 100644
--- a/app/javascript/flavours/glitch/components/logo.jsx
+++ b/app/javascript/flavours/glitch/components/logo.tsx
@@ -1,14 +1,12 @@
import logo from '@/images/logo.svg';
-export const WordmarkLogo = () => (
+export const WordmarkLogo: React.FC = () => (
);
-export const SymbolLogo = () => (
+export const SymbolLogo: React.FC = () => (
);
-
-export default WordmarkLogo;
diff --git a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx
index 78844f389d..d42a7d7c72 100644
--- a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx
+++ b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx
@@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl';
-import illustration from 'flavours/glitch/images/elephant_ui_working.svg';
+import illustration from '@/images/elephant_ui_working.svg';
const RegenerationIndicator = () => (
diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx
index ac3ab0fb4d..b9e1e4f8fd 100644
--- a/app/javascript/flavours/glitch/components/relative_timestamp.tsx
+++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx
@@ -53,7 +53,6 @@ const messages = defineMessages({
});
const dateFormatOptions = {
- hour12: false,
year: 'numeric',
month: 'short',
day: '2-digit',
@@ -103,7 +102,7 @@ const getUnitDelay = (units: string) => {
};
export const timeAgoString = (
- intl: IntlShape,
+ intl: Pick
,
date: Date,
now: number,
year: number,
diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx
index 81cc5a9d12..4d4019af0f 100644
--- a/app/javascript/flavours/glitch/components/status.jsx
+++ b/app/javascript/flavours/glitch/components/status.jsx
@@ -20,6 +20,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
+import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
import { displayMedia, visibleReactions } from '../initial_state';
import AttachmentList from './attachment_list';
@@ -73,9 +74,13 @@ export const defaultMediaVisibility = (status, settings) => {
class Status extends ImmutablePureComponent {
+<<<<<<< HEAD
static contextTypes = {
identity: PropTypes.object,
};
+=======
+ static contextType = SensitiveMediaContext;
+>>>>>>> 3341db939cd077820ad598b0445d02ab2382eaf4
static propTypes = {
containerId: PropTypes.string,
@@ -132,8 +137,7 @@ class Status extends ImmutablePureComponent {
isCollapsed: false,
autoCollapsed: false,
isExpanded: undefined,
- showMedia: undefined,
- statusId: undefined,
+ showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
revealBehindCW: undefined,
showCard: false,
forceFilter: undefined,
@@ -218,12 +222,6 @@ class Status extends ImmutablePureComponent {
updated = true;
}
- if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
- update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
- update.statusId = nextProps.status.get('id');
- updated = true;
- }
-
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
if (update.revealBehindCW) {
@@ -319,6 +317,18 @@ class Status extends ImmutablePureComponent {
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
+
+ // This will potentially cause a wasteful redraw, but in most cases `Status` components are used
+ // with a `key` directly depending on their `id`, preventing re-use of the component across
+ // different IDs.
+ // But just in case this does change, reset the state on status change.
+
+ if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
+ this.setState({
+ showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
+ forceFilter: undefined,
+ });
+ }
}
componentWillUnmount() {
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx
index f115f88b5c..61b898cda7 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.jsx
+++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx
@@ -381,7 +381,7 @@ class StatusActionBar extends ImmutablePureComponent {
- {status.get('edited_at') && *}
+ {status.get('edited_at') && *}
);
diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx
index 52aac5ebe4..a7b20bc249 100644
--- a/app/javascript/flavours/glitch/containers/media_container.jsx
+++ b/app/javascript/flavours/glitch/containers/media_container.jsx
@@ -80,7 +80,7 @@ export default class MediaContainer extends PureComponent {
return (
<>
- {[].map.call(components, (component, i) => {
+ {Array.from(components).map((component, i) => {
const componentName = component.getAttribute('data-component');
const Component = MEDIA_COMPONENTS[componentName];
const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props'));
diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx
index 98a2f365fb..b0cc1c4e6d 100644
--- a/app/javascript/flavours/glitch/features/about/index.jsx
+++ b/app/javascript/flavours/glitch/features/about/index.jsx
@@ -10,7 +10,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
-
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
@@ -171,7 +170,8 @@ class About extends PureComponent {
{server.get('rules').map(rule => (
-
- {rule.get('text')}
+
{rule.get('text')}
+ {rule.get('hint').length > 0 && ({rule.get('hint')}
)}
))}
@@ -189,18 +189,20 @@ class About extends PureComponent {
<>
-
- {domainBlocks.get('items').map(block => (
-
-
-
{block.get('domain')}
- {intl.formatMessage(severityMessages[block.get('severity')].title)}
-
+ {domainBlocks.get('items').size > 0 && (
+
+ {domainBlocks.get('items').map(block => (
+
+
+
{block.get('domain')}
+ {intl.formatMessage(severityMessages[block.get('severity')].title)}
+
-
{(block.get('comment') || '').length > 0 ? block.get('comment') : }
-
- ))}
-
+
{(block.get('comment') || '').length > 0 ? block.get('comment') : }
+
+ ))}
+
+ )}
>
) : (
diff --git a/app/javascript/flavours/glitch/features/account/components/domain_pill.jsx b/app/javascript/flavours/glitch/features/account/components/domain_pill.jsx
new file mode 100644
index 0000000000..9cd028fa68
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/components/domain_pill.jsx
@@ -0,0 +1,86 @@
+import PropTypes from 'prop-types';
+import { useState, useRef, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import Overlay from 'react-overlays/Overlay';
+
+
+
+import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
+import BadgeIcon from '@/material-icons/400-24px/badge.svg?react';
+import GlobeIcon from '@/material-icons/400-24px/globe.svg?react';
+import { Icon } from 'flavours/glitch/components/icon';
+
+export const DomainPill = ({ domain, username, isSelf }) => {
+ const [open, setOpen] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const triggerRef = useRef(null);
+
+ const handleClick = useCallback(() => {
+ setOpen(!open);
+ }, [open, setOpen]);
+
+ const handleExpandClick = useCallback(() => {
+ setExpanded(!expanded);
+ }, [expanded, setExpanded]);
+
+ return (
+ <>
+
+
+
+ {({ props }) => (
+
+
+
+
+
{isSelf ? : }
+
@{username}@{domain}
+
+
+
+
+
{isSelf ? }} /> : }} />}
+
+ {expanded && (
+ <>
+
+
+ >
+ )}
+
+ )}
+
+ >
+ );
+};
+
+DomainPill.propTypes = {
+ username: PropTypes.string.isRequired,
+ domain: PropTypes.string.isRequired,
+ isSelf: PropTypes.bool,
+};
diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx
index 29e3aa32f7..b97de5aeab 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.jsx
+++ b/app/javascript/flavours/glitch/features/account/components/header.jsx
@@ -21,8 +21,9 @@ import { Button } from 'flavours/glitch/components/button';
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
+import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
-import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
+import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@@ -30,6 +31,8 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
+import { DomainPill } from './domain_pill';
+
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -74,7 +77,7 @@ const messages = defineMessages({
const titleFromAccount = account => {
const displayName = account.get('display_name');
- const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
+ const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`;
@@ -84,7 +87,6 @@ const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
- hour12: false,
hour: '2-digit',
minute: '2-digit',
};
@@ -167,7 +169,7 @@ class Header extends ImmutablePureComponent {
};
render () {
- const { account, hidden, intl, domain } = this.props;
+ const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.context.identity;
if (!account) {
@@ -207,7 +209,7 @@ class Header extends ImmutablePureComponent {
if (me !== account.get('id')) {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
- actionBtn = '';
+ actionBtn = ;
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = ;
} else if (!account.getIn(['relationship', 'blocking'])) {
@@ -314,7 +316,8 @@ class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1;
- const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
+ const username = account.get('acct').split('@')[0];
+ const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
const isIndexable = !account.get('noindex');
const badges = [];
@@ -348,15 +351,10 @@ class Header extends ImmutablePureComponent {
- {!hidden && (
- <>
- {actionBtn}
- {bellBtn}
- {shareBtn}
- >
- )}
-
+ {!hidden && bellBtn}
+ {!hidden && shareBtn}
+ {!hidden && actionBtn}
@@ -364,7 +362,9 @@ class Header extends ImmutablePureComponent {
- @{acct} {lockedIcon}
+ @{username}@{domain}
+
+ {lockedIcon}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
index ebd156a4f9..c709e58db1 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
@@ -12,7 +12,6 @@ import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
-
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
index eb2ddbdd80..37258d9d83 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
@@ -72,11 +72,7 @@ class Header extends ImmutablePureComponent {
};
handleBlockDomain = () => {
- const domain = this.props.account.get('acct').split('@')[1];
-
- if (!domain) return;
-
- this.props.onBlockDomain(domain);
+ this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx
index c3a3de71ed..d42bb2c251 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx
@@ -15,7 +15,7 @@ import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
-import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
+import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
@@ -138,15 +138,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
- onBlockDomain (domain) {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: {domain} }} />,
- confirm: intl.formatMessage(messages.blockDomainConfirm),
- onConfirm: () => dispatch(blockDomain(domain)),
- },
- }));
+ onBlockDomain (account) {
+ dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {
diff --git a/app/javascript/flavours/glitch/features/audio/index.jsx b/app/javascript/flavours/glitch/features/audio/index.jsx
index dc9d76222d..6b0c2f6fea 100644
--- a/app/javascript/flavours/glitch/features/audio/index.jsx
+++ b/app/javascript/flavours/glitch/features/audio/index.jsx
@@ -18,7 +18,6 @@ import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
-
import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state';
diff --git a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx
index 1e93125d59..a13081e82b 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx
@@ -26,7 +26,7 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props;
return (
-
+
} />
diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx
index e2b982aaf8..7be2196511 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx
@@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
-
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state';
diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx
index 8dc89084d9..b97c3e7c85 100644
--- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx
@@ -332,6 +332,7 @@ class EmojiPickerDropdown extends PureComponent {
state = {
active: false,
loading: false,
+ placement: 'bottom',
};
setRef = (c) => {
@@ -383,10 +384,14 @@ class EmojiPickerDropdown extends PureComponent {
return this.target;
};
+ handleOverlayEnter = (state) => {
+ this.setState({ placement: state.placement });
+ };
+
render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
- const { active, loading } = this.state;
+ const { active, loading, placement } = this.state;
return (
@@ -399,7 +404,7 @@ class EmojiPickerDropdown extends PureComponent {
inverted
/>
-
+
{({ props, placement })=> (
diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx
index e757b9162a..361522d7b4 100644
--- a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx
@@ -59,10 +59,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
const dispatch = useDispatch();
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
const lang = useSelector(state => state.getIn(['compose', 'language']));
+ const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
const handleChange = useCallback(({ target: { value } }) => {
- dispatch(changePollOption(index, value));
- }, [dispatch, index]);
+ dispatch(changePollOption(index, value, maxOptions));
+ }, [dispatch, index, maxOptions]);
const handleSuggestionsFetchRequested = useCallback(token => {
dispatch(fetchComposeSuggestions(token));
diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.jsx b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx
index b759eac90b..3df025db55 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search_results.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx
@@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
-import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadMore } from 'flavours/glitch/components/load_more';
@@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
return (
-
-
-
-
-
{accounts}
{hashtags}
{statuses}
diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
index 2082510f12..2e835464b8 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
@@ -4,14 +4,24 @@ import { uploadCompose } from '../../../actions/compose';
import { openModal } from '../../../actions/modal';
import UploadButton from '../components/upload_button';
-const mapStateToProps = state => ({
- disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
- resetFileKey: state.getIn(['compose', 'resetFileKey']),
-});
+const mapStateToProps = state => {
+ const isPoll = state.getIn(['compose', 'poll']) !== null;
+ const isUploading = state.getIn(['compose', 'is_uploading']);
+ const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
+ const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
+ const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
+ const isOverLimit = attachmentsSize > 3;
+ const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
+
+ return {
+ disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio,
+ resetFileKey: state.getIn(['compose', 'resetFileKey']),
+ };
+};
const mapDispatchToProps = dispatch => ({
- onSelectFile (files) {
+ onSelectFile(files) {
dispatch(uploadCompose(files));
},
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.jsx
index 9c8e23ce73..45de7010b6 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.jsx
@@ -26,18 +26,20 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props;
return (
-
-
+
);
}
diff --git a/app/javascript/flavours/glitch/features/directory/index.jsx b/app/javascript/flavours/glitch/features/directory/index.jsx
index 0d8742b255..293b89272a 100644
--- a/app/javascript/flavours/glitch/features/directory/index.jsx
+++ b/app/javascript/flavours/glitch/features/directory/index.jsx
@@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
-
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_compressed.js b/app/javascript/flavours/glitch/features/emoji/emoji_compressed.js
index 792137b76f..59978a391b 100644
--- a/app/javascript/flavours/glitch/features/emoji/emoji_compressed.js
+++ b/app/javascript/flavours/glitch/features/emoji/emoji_compressed.js
@@ -36,7 +36,7 @@ Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
- if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
+ if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
@@ -88,7 +88,7 @@ Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
- if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
+ if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_utils.js b/app/javascript/flavours/glitch/features/emoji/emoji_utils.js
index 83bcc9d82f..c13d250567 100644
--- a/app/javascript/flavours/glitch/features/emoji/emoji_utils.js
+++ b/app/javascript/flavours/glitch/features/emoji/emoji_utils.js
@@ -135,19 +135,19 @@ function getData(emoji, skin, set) {
}
}
- if (Object.prototype.hasOwnProperty.call(data.short_names, emoji)) {
+ if (Object.hasOwn(data.short_names, emoji)) {
emoji = data.short_names[emoji];
}
- if (Object.prototype.hasOwnProperty.call(data.emojis, emoji)) {
+ if (Object.hasOwn(data.emojis, emoji)) {
emojiData = data.emojis[emoji];
}
} else if (emoji.id) {
- if (Object.prototype.hasOwnProperty.call(data.short_names, emoji.id)) {
+ if (Object.hasOwn(data.short_names, emoji.id)) {
emoji.id = data.short_names[emoji.id];
}
- if (Object.prototype.hasOwnProperty.call(data.emojis, emoji.id)) {
+ if (Object.hasOwn(data.emojis, emoji.id)) {
emojiData = data.emojis[emoji.id];
skin = skin || emoji.skin;
}
@@ -216,7 +216,7 @@ function deepMerge(a, b) {
let originalValue = a[key],
value = originalValue;
- if (Object.prototype.hasOwnProperty.call(b, key)) {
+ if (Object.hasOwn(b, key)) {
value = b[key];
}
diff --git a/app/javascript/flavours/glitch/features/explore/index.jsx b/app/javascript/flavours/glitch/features/explore/index.jsx
index dca2e59c5c..cca9cada47 100644
--- a/app/javascript/flavours/glitch/features/explore/index.jsx
+++ b/app/javascript/flavours/glitch/features/explore/index.jsx
@@ -8,7 +8,6 @@ import { NavLink, Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
-
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import Column from 'flavours/glitch/components/column';
diff --git a/app/javascript/flavours/glitch/features/explore/results.jsx b/app/javascript/flavours/glitch/features/explore/results.jsx
index 53285988c3..e0162f713a 100644
--- a/app/javascript/flavours/glitch/features/explore/results.jsx
+++ b/app/javascript/flavours/glitch/features/explore/results.jsx
@@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
-
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
diff --git a/app/javascript/flavours/glitch/features/filters/select_filter.jsx b/app/javascript/flavours/glitch/features/filters/select_filter.jsx
index f409cc2d70..3a46b8bd48 100644
--- a/app/javascript/flavours/glitch/features/filters/select_filter.jsx
+++ b/app/javascript/flavours/glitch/features/filters/select_filter.jsx
@@ -180,7 +180,7 @@ class SelectFilter extends PureComponent {
-
+
diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx
index 29219974a9..dc6a38ae2e 100644
--- a/app/javascript/flavours/glitch/features/firehose/index.jsx
+++ b/app/javascript/flavours/glitch/features/firehose/index.jsx
@@ -6,7 +6,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
-
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings';
@@ -46,28 +45,37 @@ const ColumnSettings = () => {
);
return (
-
-
- }
- />
- }
- />
-
-
-
+
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+
);
};
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
index 27470d9938..0ff0a863b3 100644
--- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
@@ -343,7 +343,7 @@ class Announcement extends ImmutablePureComponent {
- {hasTimeRange && · - }
+ {hasTimeRange && · - }
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx
index d27d134b30..585abb8b6c 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx
@@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
+import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@@ -22,7 +23,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
-import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { fetchLists } from 'flavours/glitch/actions/lists';
import { openModal } from 'flavours/glitch/actions/modal';
@@ -158,7 +158,7 @@ class GettingStarted extends ImmutablePureComponent {
}
if (showTrends) {
- navItems.push(
);
+ navItems.push(
);
}
if (signedIn) {
diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx
index 4488c5b2a0..94ee7bb119 100644
--- a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx
@@ -109,28 +109,28 @@ class ColumnSettings extends PureComponent {
const { settings, onChange } = this.props;
return (
-
-
-
-
+
+
+
+
} />
-
-
-
+
+
+
+
+
+
+
-
- {this.state.open && (
-
- {this.modeSelect('any')}
- {this.modeSelect('all')}
- {this.modeSelect('none')}
-
- )}
-
-
- } />
-
+ {this.state.open && (
+
+ {this.modeSelect('any')}
+ {this.modeSelect('all')}
+ {this.modeSelect('none')}
+
+ )}
+
);
}
diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.tsx b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.tsx
index 5438ba6f75..5b8fe5ccc8 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.tsx
+++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.tsx
@@ -35,75 +35,68 @@ export const ColumnSettings: React.FC = () => {
);
return (
-
-
-
-
+
+
+
+
+ }
+ />
-
-
- }
- />
-
+
+ }
+ />
-
-
- }
- />
-
+
+ }
+ />
+
+
-
-
- }
- />
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
);
};
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
index f383a6e48c..a2683f587f 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
@@ -8,7 +8,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
-
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
index 98f65d9b50..858d760c24 100644
--- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
+++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
@@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
diff --git a/app/javascript/flavours/glitch/features/list_adder/components/list.jsx b/app/javascript/flavours/glitch/features/list_adder/components/list.jsx
index adbb16fcd6..e91c8e35bd 100644
--- a/app/javascript/flavours/glitch/features/list_adder/components/list.jsx
+++ b/app/javascript/flavours/glitch/features/list_adder/components/list.jsx
@@ -11,7 +11,6 @@ import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import { IconButton } from '../../../components/icon_button';
diff --git a/app/javascript/flavours/glitch/features/list_editor/components/search.jsx b/app/javascript/flavours/glitch/features/list_editor/components/search.jsx
index 6e9cd44835..23e4b427dc 100644
--- a/app/javascript/flavours/glitch/features/list_editor/components/search.jsx
+++ b/app/javascript/flavours/glitch/features/list_editor/components/search.jsx
@@ -11,7 +11,6 @@ import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx
index ae9f75318f..08ce97f1ba 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.jsx
@@ -193,37 +193,38 @@ class ListTimeline extends PureComponent {
pinned={pinned}
multiColumn={multiColumn}
>
-
-
+
+
+
+
-
-
-
-
-
- { replies_policy !== undefined && (
-
-
-
-
-
- { ['none', 'list', 'followed'].map(policy => (
-
- ))}
+
- )}
+
-
+ {replies_policy !== undefined && (
+
+
+
+
+ { ['none', 'list', 'followed'].map(policy => (
+
+ ))}
+
+
+ )}
+
+
+
+
{
+ const handleChange = useCallback(({ target }) => {
+ onChange(target.checked);
+ }, [onChange]);
+
+ return (
+
+ );
+};
+
+CheckboxWithLabel.propTypes = {
+ checked: PropTypes.bool,
+ disabled: PropTypes.bool,
+ children: PropTypes.children,
+ onChange: PropTypes.func,
+};
diff --git a/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx
index 6e29709b59..ff4db67886 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx
@@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
export default class ClearColumnButton extends PureComponent {
static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
index 74b3c47346..4c14ca4a92 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
+import { CheckboxWithLabel } from './checkbox_with_label';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import PillBarButton from './pill_bar_button';
@@ -27,14 +28,32 @@ export default class ColumnSettings extends PureComponent {
alertsEnabled: PropTypes.bool,
browserSupport: PropTypes.bool,
browserPermission: PropTypes.string,
+ notificationPolicy: ImmutablePropTypes.map,
+ onChangePolicy: PropTypes.func.isRequired,
};
onPushChange = (path, checked) => {
this.props.onChange(['push', ...path], checked);
};
+ handleFilterNotFollowing = checked => {
+ this.props.onChangePolicy('filter_not_following', checked);
+ };
+
+ handleFilterNotFollowers = checked => {
+ this.props.onChangePolicy('filter_not_followers', checked);
+ };
+
+ handleFilterNewAccounts = checked => {
+ this.props.onChangePolicy('filter_new_accounts', checked);
+ };
+
+ handleFilterPrivateMentions = checked => {
+ this.props.onChangePolicy('filter_private_mentions', checked);
+ };
+
render () {
- const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
+ const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
const unreadMarkersShowStr = ;
const filterBarShowStr = ;
@@ -47,48 +66,70 @@ export default class ColumnSettings extends PureComponent {
const pushStr = showPushSettings && ;
return (
-
+
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
-
-
-
+
)}
+
+
{alertsEnabled && browserSupport && browserPermission === 'default' && (
-
+
+
)}
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
@@ -96,10 +137,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -107,10 +148,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -118,21 +159,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
-
-
-
- {showPushSettings &&
}
-
-
-
-
-
-
-
+
+
@@ -140,10 +170,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -151,10 +181,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -162,10 +192,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -173,10 +203,10 @@ export default class ColumnSettings extends PureComponent {
-
+
-
-
+
+
@@ -184,11 +214,11 @@ export default class ColumnSettings extends PureComponent {
-
+
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
-
-
+
+
@@ -196,12 +226,12 @@ export default class ColumnSettings extends PureComponent {
-
+
)}
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
-
-
+
+
@@ -209,7 +239,7 @@ export default class ColumnSettings extends PureComponent {
-
+
)}
);
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx
index 13aba0d0ef..9459b50ebc 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx
@@ -12,7 +12,6 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx
new file mode 100644
index 0000000000..5949662828
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx
@@ -0,0 +1,48 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
+import { fetchNotificationPolicy } from 'flavours/glitch/actions/notifications';
+import { Icon } from 'flavours/glitch/components/icon';
+import { toCappedNumber } from 'flavours/glitch/utils/numbers';
+
+export const FilteredNotificationsBanner = () => {
+ const dispatch = useDispatch();
+ const policy = useSelector(state => state.get('notificationPolicy'));
+
+ useEffect(() => {
+ dispatch(fetchNotificationPolicy());
+
+ const interval = setInterval(() => {
+ dispatch(fetchNotificationPolicy());
+ }, 120000);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [dispatch]);
+
+ if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification_request.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification_request.jsx
new file mode 100644
index 0000000000..6ba97066ce
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification_request.jsx
@@ -0,0 +1,65 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import DoneIcon from '@/material-icons/400-24px/done.svg?react';
+import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
+import { acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
+import { Avatar } from 'flavours/glitch/components/avatar';
+import { IconButton } from 'flavours/glitch/components/icon_button';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import { toCappedNumber } from 'flavours/glitch/utils/numbers';
+
+const getAccount = makeGetAccount();
+
+const messages = defineMessages({
+ accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
+ dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
+});
+
+export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
+ const dispatch = useDispatch();
+ const account = useSelector(state => getAccount(state, accountId));
+ const intl = useIntl();
+
+ const handleDismiss = useCallback(() => {
+ dispatch(dismissNotificationRequest(id));
+ }, [dispatch, id]);
+
+ const handleAccept = useCallback(() => {
+ dispatch(acceptNotificationRequest(id));
+ }, [dispatch, id]);
+
+ return (
+
+
+
+
+
+
+
+ {toCappedNumber(notificationsCount)}
+
+
+
@{account?.get('acct')}
+
+
+
+
+
+
+
+
+ );
+};
+
+NotificationRequest.propTypes = {
+ id: PropTypes.string.isRequired,
+ accountId: PropTypes.string.isRequired,
+ notificationsCount: PropTypes.string.isRequired,
+};
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx
index 82c08ea175..7d1367a623 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx
@@ -5,9 +5,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-
+import SettingsIcon from '@/material-icons/400-20px/settings.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { Button } from 'flavours/glitch/components/button';
@@ -43,7 +42,7 @@ class NotificationsPermissionBanner extends PureComponent {
-
}} />
+
}} />
);
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
index 1e62ed9a5a..de266160f8 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { showAlert } from '../../../actions/alerts';
import { openModal } from '../../../actions/modal';
-import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
+import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { changeSetting } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings';
@@ -21,6 +21,7 @@ const mapStateToProps = state => ({
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
browserSupport: state.getIn(['notifications', 'browserSupport']),
browserPermission: state.getIn(['notifications', 'browserPermission']),
+ notificationPolicy: state.get('notificationPolicy'),
});
const mapDispatchToProps = (dispatch, { intl }) => ({
@@ -73,6 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(requestBrowserPermission());
},
+ onChangePolicy (param, checked) {
+ dispatch(updateNotificationsPolicy({
+ [param]: checked,
+ }));
+ },
+
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
diff --git a/app/javascript/flavours/glitch/features/notifications/index.jsx b/app/javascript/flavours/glitch/features/notifications/index.jsx
index 783c35a43e..e84ef70b05 100644
--- a/app/javascript/flavours/glitch/features/notifications/index.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/index.jsx
@@ -37,6 +37,7 @@ import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
+import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container';
@@ -357,6 +358,9 @@ class Notifications extends PureComponent {
{filterBarContainer}
+
+
+
{scrollContainer}
diff --git a/app/javascript/flavours/glitch/features/notifications/request.jsx b/app/javascript/flavours/glitch/features/notifications/request.jsx
new file mode 100644
index 0000000000..4a6a189b8f
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/request.jsx
@@ -0,0 +1,147 @@
+import PropTypes from 'prop-types';
+import { useRef, useCallback, useEffect } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import DoneIcon from '@/material-icons/400-24px/done.svg?react';
+import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
+import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
+import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { IconButton } from 'flavours/glitch/components/icon_button';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
+import { SensitiveMediaContextProvider } from 'flavours/glitch/features/ui/util/sensitive_media_context';
+
+import NotificationContainer from './containers/notification_container';
+
+const messages = defineMessages({
+ title: { id: 'notification_requests.notifications_from', defaultMessage: 'Notifications from {name}' },
+ accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
+ dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
+});
+
+const selectChild = (ref, index, alignTop) => {
+ const container = ref.current.node;
+ const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+ if (element) {
+ if (alignTop && container.scrollTop > element.offsetTop) {
+ element.scrollIntoView(true);
+ } else if (!alignTop && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+ element.scrollIntoView(false);
+ }
+
+ element.focus();
+ }
+};
+
+export const NotificationRequest = ({ multiColumn, params: { id } }) => {
+ const columnRef = useRef();
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
+ const accountId = notificationRequest?.get('account');
+ const account = useSelector(state => state.getIn(['accounts', accountId]));
+ const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
+ const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
+ const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
+ const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
+
+ const handleHeaderClick = useCallback(() => {
+ columnRef.current?.scrollTop();
+ }, [columnRef]);
+
+ const handleLoadMore = useCallback(() => {
+ dispatch(expandNotificationsForRequest());
+ }, [dispatch]);
+
+ const handleDismiss = useCallback(() => {
+ dispatch(dismissNotificationRequest(id));
+ }, [dispatch, id]);
+
+ const handleAccept = useCallback(() => {
+ dispatch(acceptNotificationRequest(id));
+ }, [dispatch, id]);
+
+ const handleMoveUp = useCallback(id => {
+ const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+ selectChild(columnRef, elementIndex, true);
+ }, [columnRef, notifications]);
+
+ const handleMoveDown = useCallback(id => {
+ const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+ selectChild(columnRef, elementIndex, false);
+ }, [columnRef, notifications]);
+
+ useEffect(() => {
+ dispatch(fetchNotificationRequest(id));
+ }, [dispatch, id]);
+
+ useEffect(() => {
+ if (accountId) {
+ dispatch(fetchNotificationsForRequest(accountId));
+ }
+ }, [dispatch, accountId]);
+
+ const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') || account?.get('username') });
+
+ return (
+
+
+
+
+ >
+ )}
+ />
+
+
+
+ {notifications.map(item => (
+ item &&
+ ))}
+
+
+
+
+ {columnTitle}
+
+
+
+ );
+};
+
+NotificationRequest.propTypes = {
+ multiColumn: PropTypes.bool,
+ params: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ }),
+};
+
+export default NotificationRequest;
diff --git a/app/javascript/flavours/glitch/features/notifications/requests.jsx b/app/javascript/flavours/glitch/features/notifications/requests.jsx
new file mode 100644
index 0000000000..d7a07da152
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/requests.jsx
@@ -0,0 +1,85 @@
+import PropTypes from 'prop-types';
+import { useRef, useCallback, useEffect } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
+import { fetchNotificationRequests, expandNotificationRequests } from 'flavours/glitch/actions/notifications';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
+
+import { NotificationRequest } from './components/notification_request';
+
+const messages = defineMessages({
+ title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
+});
+
+export const NotificationRequests = ({ multiColumn }) => {
+ const columnRef = useRef();
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
+ const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
+ const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
+
+ const handleHeaderClick = useCallback(() => {
+ columnRef.current?.scrollTop();
+ }, [columnRef]);
+
+ const handleLoadMore = useCallback(() => {
+ dispatch(expandNotificationRequests());
+ }, [dispatch]);
+
+ useEffect(() => {
+ dispatch(fetchNotificationRequests());
+ }, [dispatch]);
+
+ return (
+
+
+
+ }
+ >
+ {notificationRequests.map(request => (
+
+ ))}
+
+
+
+ {intl.formatMessage(messages.title)}
+
+
+
+ );
+};
+
+NotificationRequests.propTypes = {
+ multiColumn: PropTypes.bool,
+};
+
+export default NotificationRequests;
diff --git a/app/javascript/flavours/glitch/features/onboarding/components/step.jsx b/app/javascript/flavours/glitch/features/onboarding/components/step.jsx
index 4dcd3c9ce5..32648982a3 100644
--- a/app/javascript/flavours/glitch/features/onboarding/components/step.jsx
+++ b/app/javascript/flavours/glitch/features/onboarding/components/step.jsx
@@ -6,7 +6,6 @@ import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?rea
import CheckIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
const content = (
<>
diff --git a/app/javascript/flavours/glitch/features/onboarding/index.jsx b/app/javascript/flavours/glitch/features/onboarding/index.jsx
index fb9df29aeb..187ee3b770 100644
--- a/app/javascript/flavours/glitch/features/onboarding/index.jsx
+++ b/app/javascript/flavours/glitch/features/onboarding/index.jsx
@@ -8,7 +8,6 @@ import { Link, Switch, Route, useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
-
import illustration from '@/images/elephant_ui_conversation.svg';
import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
index 83e53d9263..3bf4f3b857 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
@@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx
index dd90cd3522..5996ab240d 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx
@@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'flavours/glitch/components/avatar';
import { DisplayName } from 'flavours/glitch/components/display_name';
diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx
index 94d5f6cb4c..7b4e36cdaf 100644
--- a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx
+++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx
@@ -11,7 +11,6 @@ import { connect } from 'react-redux';
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import { getStatusList } from 'flavours/glitch/selectors';
-
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
diff --git a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx
index 82684c8368..63c14b897b 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx
@@ -25,18 +25,22 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props;
return (
-
-
- } />
- } />
- {!settings.getIn(['other', 'onlyRemote']) && } />}
-
+
+
+
+ } />
+ } />
+ {!settings.getIn(['other', 'onlyRemote']) && } />}
+
+
-
+
);
}
diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx
index 0062a9f719..8b9503928b 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx
@@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
-
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state';
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.jsx b/app/javascript/flavours/glitch/features/reblogs/index.jsx
index 52e9a35833..3c02293e58 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.jsx
+++ b/app/javascript/flavours/glitch/features/reblogs/index.jsx
@@ -14,7 +14,6 @@ import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
import ColumnHeader from '../../components/column_header';
import { LoadingIndicator } from '../../components/loading_indicator';
diff --git a/app/javascript/flavours/glitch/features/report/components/option.jsx b/app/javascript/flavours/glitch/features/report/components/option.jsx
index 3b40de8cba..8edeed28ad 100644
--- a/app/javascript/flavours/glitch/features/report/components/option.jsx
+++ b/app/javascript/flavours/glitch/features/report/components/option.jsx
@@ -6,7 +6,6 @@ import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
-
export default class Option extends PureComponent {
static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx b/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx
index 0ae20b8f48..06ed6ac33c 100644
--- a/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx
@@ -19,6 +19,7 @@ class StatusCheckBox extends PureComponent {
status: ImmutablePropTypes.map.isRequired,
checked: PropTypes.bool,
onToggle: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
};
handleStatusesToggle = (value, checked) => {
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
index be33728ace..aa0ce6d1ba 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
@@ -18,8 +18,8 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
-import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg';
-import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg';
+import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
+import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx
index c4461f4378..4c535b0a46 100644
--- a/app/javascript/flavours/glitch/features/status/components/card.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/card.jsx
@@ -82,6 +82,10 @@ export default class Card extends PureComponent {
this.setState({ embedded: true });
};
+ handleExternalLinkClick = (e) => {
+ e.stopPropagation();
+ };
+
setRef = c => {
this.node = c;
};
@@ -191,7 +195,7 @@ export default class Card extends PureComponent {
) : spoilerButton}
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
index 38001ae9bc..01a47639e6 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
-import { FormattedDate } from 'react-intl';
+import { FormattedDate, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link, withRouter } from 'react-router-dom';
@@ -8,14 +8,10 @@ import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
-import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
-import { Icon } from 'flavours/glitch/components/icon';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container';
@@ -140,10 +136,7 @@ class DetailedStatus extends ImmutablePureComponent {
let applicationLink = '';
let reblogLink = '';
- const reblogIcon = 'retweet';
- const reblogIconComponent = RepeatIcon;
let favouriteLink = '';
- let edited = '';
// Depending on user settings, some media are considered as parts of the
// contents (affected by CW) while other will be displayed outside of the
@@ -246,68 +239,53 @@ class DetailedStatus extends ImmutablePureComponent {
}
if (status.get('application')) {
- applicationLink = <> ·
{status.getIn(['application', 'name'])}>;
+ applicationLink = <>·
{status.getIn(['application', 'name'])}>;
}
- const visibilityLink = <> ·
>;
+ const visibilityLink = <>·
>;
if (!['unlisted', 'public'].includes(status.get('visibility'))) {
reblogLink = null;
} else if (this.props.history) {
reblogLink = (
- <>
- {' · '}
-
-
-
-
-
-
- >
+
+
+
+
+
+
);
} else {
reblogLink = (
- <>
- {' · '}
-
-
-
-
-
-
- >
+
+
+
+
+
+
);
}
if (this.props.history) {
favouriteLink = (
-
+
);
} else {
favouriteLink = (
-
+
);
}
- if (status.get('edited_at')) {
- edited = (
- <>
- {' · '}
-
- >
- );
- }
-
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
@@ -345,9 +323,23 @@ class DetailedStatus extends ImmutablePureComponent {
/>
-
-
- {edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
+
+
+
+
+
+ {visibilityLink}
+
+ {applicationLink}
+
+
+ {status.get('edited_at') &&
}
+
+
+ {reblogLink}
+ {reblogLink && <>·>}
+ {favouriteLink}
+
diff --git a/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx b/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx
index 1edfe700fe..c02a6ed037 100644
--- a/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx
+++ b/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx
@@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { followAccount } from 'flavours/glitch/actions/accounts';
import { Button } from 'flavours/glitch/components/button';
diff --git a/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
index cfac692324..fa772b067d 100644
--- a/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
@@ -1,100 +1,116 @@
import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
+import { useCallback, useState } from 'react';
-import { injectIntl, FormattedMessage } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
+import classNames from 'classnames';
-import { blockAccount } from '../../../actions/accounts';
-import { closeModal } from '../../../actions/modal';
-import { initReport } from '../../../actions/reports';
-import { Button } from '../../../components/button';
-import { makeGetAccount } from '../../../selectors';
+import { useDispatch } from 'react-redux';
-const makeMapStateToProps = () => {
- const getAccount = makeGetAccount();
+import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
+import BlockIcon from '@/material-icons/400-24px/block.svg?react';
+import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
+import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
+import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
+import { blockAccount } from 'flavours/glitch/actions/accounts';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { Button } from 'flavours/glitch/components/button';
+import { Icon } from 'flavours/glitch/components/icon';
- const mapStateToProps = state => ({
- account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
- });
+export const BlockModal = ({ accountId, acct }) => {
+ const dispatch = useDispatch();
+ const [expanded, setExpanded] = useState(false);
- return mapStateToProps;
-};
+ const domain = acct.split('@')[1];
-const mapDispatchToProps = dispatch => {
- return {
- onConfirm(account) {
- dispatch(blockAccount(account.get('id')));
- },
+ const handleClick = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ dispatch(blockAccount(accountId));
+ }, [dispatch, accountId]);
- onBlockAndReport(account) {
- dispatch(blockAccount(account.get('id')));
- dispatch(initReport(account));
- },
+ const handleCancel = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ }, [dispatch]);
- onClose() {
- dispatch(closeModal({
- modalType: undefined,
- ignoreFocus: false,
- }));
- },
- };
-};
+ const handleToggleLearnMore = useCallback(() => {
+ setExpanded(!expanded);
+ }, [expanded, setExpanded]);
-class BlockModal extends PureComponent {
+ return (
+
+
+
+
+
+
- static propTypes = {
- account: PropTypes.object.isRequired,
- onClose: PropTypes.func.isRequired,
- onBlockAndReport: PropTypes.func.isRequired,
- onConfirm: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- };
-
- handleClick = () => {
- this.props.onClose();
- this.props.onConfirm(this.props.account);
- };
-
- handleSecondary = () => {
- this.props.onClose();
- this.props.onBlockAndReport(this.props.account);
- };
-
- handleCancel = () => {
- this.props.onClose();
- };
-
- render () {
- const { account } = this.props;
-
- return (
-
-
-
- @{account.get('acct')} }}
- />
-
+
-
+
+
+ {domain && (
+
+ )}
+
+
+ {domain && (
+
+ )}
+
+
+
+
-
-
+
+
- );
- }
+
+ );
+};
-}
+BlockModal.propTypes = {
+ accountId: PropTypes.string.isRequired,
+ acct: PropTypes.string.isRequired,
+};
-export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(BlockModal));
+export default BlockModal;
diff --git a/app/javascript/flavours/glitch/features/ui/components/column_link.jsx b/app/javascript/flavours/glitch/features/ui/components/column_link.jsx
index f42ff5a6e6..4445435309 100644
--- a/app/javascript/flavours/glitch/features/ui/components/column_link.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/column_link.jsx
@@ -1,27 +1,30 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { NavLink } from 'react-router-dom';
+import { useRouteMatch, NavLink } from 'react-router-dom';
import { Icon } from 'flavours/glitch/components/icon';
-const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
+const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
+ const match = useRouteMatch(to);
const className = classNames('column-link', { 'column-link--transparent': transparent });
const badgeElement = typeof badge !== 'undefined' ?
{badge} : null;
const iconElement = (typeof icon === 'string' || iconComponent) ?
: icon;
+ const activeIconElement = activeIcon ?? (activeIconComponent ?
: iconElement);
+ const active = match?.isExact;
if (href) {
return (
- {iconElement}
+ {active ? activeIconElement : iconElement}
{text}
{badgeElement}
);
} else if (to) {
return (
-
- {iconElement}
+
+ {active ? activeIconElement : iconElement}
{text}
{badgeElement}
@@ -46,6 +49,8 @@ const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badg
ColumnLink.propTypes = {
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
iconComponent: PropTypes.func,
+ activeIcon: PropTypes.node,
+ activeIconComponent: PropTypes.func,
text: PropTypes.string.isRequired,
to: PropTypes.string,
onClick: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
index f3e1bfe492..9166499c5a 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
@@ -58,6 +58,7 @@ const TabsBarPortal = () => {
export default class ColumnsArea extends ImmutablePureComponent {
static propTypes = {
columns: ImmutablePropTypes.list.isRequired,
+ isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
openSettings: PropTypes.func,
@@ -145,7 +146,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
};
render () {
- const { columns, children, singleColumn, openSettings } = this.props;
+ const { columns, children, singleColumn, isModalOpen, openSettings } = this.props;
const { renderComposePanel } = this.state;
if (singleColumn) {
@@ -172,7 +173,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
}
return (
-
+
{columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
diff --git a/app/javascript/flavours/glitch/features/ui/components/domain_block_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/domain_block_modal.jsx
new file mode 100644
index 0000000000..b1ab81dab5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/domain_block_modal.jsx
@@ -0,0 +1,106 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { useDispatch } from 'react-redux';
+
+import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
+import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
+import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
+import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
+import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
+import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
+import { blockAccount } from 'flavours/glitch/actions/accounts';
+import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { Button } from 'flavours/glitch/components/button';
+import { Icon } from 'flavours/glitch/components/icon';
+
+export const DomainBlockModal = ({ domain, accountId, acct }) => {
+ const dispatch = useDispatch();
+
+ const handleClick = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ dispatch(blockDomain(domain));
+ }, [dispatch, domain]);
+
+ const handleSecondaryClick = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ dispatch(blockAccount(accountId));
+ }, [dispatch, accountId]);
+
+ const handleCancel = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ }, [dispatch]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+DomainBlockModal.propTypes = {
+ domain: PropTypes.string.isRequired,
+ accountId: PropTypes.string.isRequired,
+ acct: PropTypes.string.isRequired,
+};
+
+export default DomainBlockModal;
diff --git a/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx
index edd2fb8940..87316b7f23 100644
--- a/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx
@@ -4,7 +4,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
-
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import api from 'flavours/glitch/api';
import { IconButton } from 'flavours/glitch/components/icon_button';
diff --git a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
index 9ce5eec874..92f83d8d59 100644
--- a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
@@ -5,7 +5,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { fetchFilters, createFilter, createFilterStatus } from 'flavours/glitch/actions/filters';
import { fetchStatus } from 'flavours/glitch/actions/statuses';
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
index d584277dc3..bf31c137f1 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
@@ -181,14 +181,14 @@ class FocalPointModal extends ImmutablePureComponent {
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- e.stopPropagation();
this.props.onChangeDescription(e.target.value);
- this.handleSubmit();
+ this.handleSubmit(e);
}
};
- handleSubmit = () => {
+ handleSubmit = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
};
@@ -318,7 +318,7 @@ class FocalPointModal extends ImmutablePureComponent {
-
-
+
-
+