mirror of
https://github.com/funamitech/mastodon
synced 2024-12-04 01:39:01 +09:00
Merge branch 'main' of https://github.com/glitch-soc/mastodon
This commit is contained in:
commit
d255c9e53e
@ -1,7 +1,9 @@
|
||||
[production]
|
||||
defaults
|
||||
not IE 11
|
||||
> 0.2%
|
||||
ios >= 15.6
|
||||
not dead
|
||||
not OperaMini all
|
||||
|
||||
[development]
|
||||
supports es6-module
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
|
4
.github/codecov.yml
vendored
4
.github/codecov.yml
vendored
@ -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
|
||||
|
18
.github/workflows/format-check.yml
vendored
Normal file
18
.github/workflows/format-check.yml
vendored
Normal file
@ -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
|
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
@ -43,4 +43,4 @@ jobs:
|
||||
- run: echo "::add-matcher::.github/stylelint-matcher.json"
|
||||
|
||||
- name: Stylelint
|
||||
run: yarn lint:sass
|
||||
run: yarn lint:css
|
||||
|
38
.github/workflows/lint-json.yml
vendored
38
.github/workflows/lint-json.yml
vendored
@ -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
|
38
.github/workflows/lint-md.yml
vendored
38
.github/workflows/lint-md.yml
vendored
@ -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
|
40
.github/workflows/lint-yml.yml
vendored
40
.github/workflows/lint-yml.yml
vendored
@ -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
|
@ -14,3 +14,5 @@ linters:
|
||||
enabled: true
|
||||
LineLength:
|
||||
max: 320
|
||||
ViewLength:
|
||||
max: 200 # Override default value of 100 inherited from rubocop
|
||||
|
@ -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'
|
@ -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
|
||||
|
@ -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"
|
||||
|
3
Gemfile
3
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
|
||||
|
43
Gemfile.lock
43
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)
|
||||
|
148
README.md
148
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.
|
||||
|
||||
---
|
||||
|
||||
<h1><picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./lib/assets/wordmark.dark.png?raw=true">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./lib/assets/wordmark.light.png?raw=true">
|
||||
<img alt="Mastodon" src="./lib/assets/wordmark.light.png?raw=true" height="34">
|
||||
</picture></h1>
|
||||
|
||||
[![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
|
||||
|
||||
<img src="/app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" />
|
||||
|
||||
### 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:<br>
|
||||
[![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 <https://www.gnu.org/licenses/>.
|
||||
>>>>>>> 3341db939cd077820ad598b0445d02ab2382eaf4
|
||||
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.post_up_message = <<MESSAGE
|
||||
To start server
|
||||
$ vagrant ssh -c "cd /vagrant && foreman start"
|
||||
$ vagrant ssh -c "cd /vagrant && bin/dev"
|
||||
MESSAGE
|
||||
|
||||
end
|
||||
|
@ -1,6 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::BaseController < Api::BaseController
|
||||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :require_authenticated_user!
|
||||
skip_before_action :require_not_suspended!
|
||||
skip_around_action :set_locale
|
||||
|
@ -1,9 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ClaimsController < ActivityPub::BaseController
|
||||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :require_account_signature!
|
||||
|
@ -1,9 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
|
@ -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!
|
||||
|
@ -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!
|
||||
|
@ -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?
|
||||
|
@ -1,9 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
include SignatureVerification
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
|
||||
DESCENDANTS_LIMIT = 60
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
37
app/controllers/api/v1/notifications/policies_controller.rb
Normal file
37
app/controllers/api/v1/notifications/policies_controller.rb
Normal file
@ -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
|
75
app/controllers/api/v1/notifications/requests_controller.rb
Normal file
75
app/controllers/api/v1/notifications/requests_controller.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
52
app/controllers/concerns/api/error_handling.rb
Normal file
52
app/controllers/concerns/api/error_handling.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
340
app/javascript/core/admin.ts
Normal file
340
app/javascript/core/admin.ts
Normal file
@ -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<HTMLInputElement>(
|
||||
'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<HTMLInputElement>(
|
||||
'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<HTMLInputElement>(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<HTMLInputElement>(
|
||||
'#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<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(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<HTMLInputElement>(
|
||||
'#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<HTMLElement>(
|
||||
'.form_admin_settings_registrations_mode .warning-hint',
|
||||
)
|
||||
.forEach((warning_hint) => {
|
||||
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(
|
||||
'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<HTMLSelectElement>(
|
||||
'select#domain_block_severity',
|
||||
);
|
||||
if (domainBlockSeveritySelect)
|
||||
onDomainBlockSeverityChange(domainBlockSeveritySelect);
|
||||
|
||||
const enableBootstrapTimelineAccounts =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
);
|
||||
if (enableBootstrapTimelineAccounts)
|
||||
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
||||
|
||||
const registrationMode = document.querySelector<HTMLSelectElement>(
|
||||
'select#form_admin_settings_registrations_mode',
|
||||
);
|
||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(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<HTMLInputElement>(
|
||||
'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<HTMLInputElement>('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<HTMLInputElement>('input[type="datetime-local"]')
|
||||
.forEach((element) => {
|
||||
if (element.value && element.validity.valid) {
|
||||
element.value = convertLocalDatetimeToUTC(element.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const announcementStartsAt = document.querySelector<HTMLInputElement>(
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
);
|
||||
if (announcementStartsAt) {
|
||||
setAnnouncementEndsAttributes(announcementStartsAt);
|
||||
}
|
||||
}).catch((reason) => {
|
||||
throw reason;
|
||||
});
|
@ -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);
|
||||
}
|
||||
});
|
41
app/javascript/core/embed.ts
Normal file
41
app/javascript/core/embed.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
70
app/javascript/core/settings.ts
Normal file
70
app/javascript/core/settings.ts
Normal file
@ -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<HTMLImageElement>(
|
||||
`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<HTMLInputElement>(
|
||||
'.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;
|
||||
});
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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'),
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
@ -266,12 +266,14 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
||||
insertIfOnline('direct');
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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'),
|
||||
},
|
||||
}));
|
||||
|
@ -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'),
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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 {
|
||||
|
39
app/javascript/flavours/glitch/components/check_box.tsx
Normal file
39
app/javascript/flavours/glitch/components/check_box.tsx
Normal file
@ -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<HTMLInputElement>) => void;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CheckBox: React.FC<Props> = ({
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<label className='check-box'>
|
||||
<input
|
||||
name={name}
|
||||
type='checkbox'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('check-box__input', { checked })}>
|
||||
{checked && <Icon id='check' icon={DoneIcon} />}
|
||||
</span>
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
};
|
@ -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;
|
||||
if (!showButton) return null;
|
||||
|
||||
return (<button onClick={handleBackClick} className='column-header__back-button'>
|
||||
return (
|
||||
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
|
||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>);
|
||||
|
||||
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
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 = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
<div className='column-header__setting-arrows'>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
backButton = <BackButton pinned={pinned} show={showBackButton} />;
|
||||
backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
|
||||
|
||||
const collapsedContent = [
|
||||
extraContent,
|
||||
];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(pinButton);
|
||||
collapsedContent.push(moveButtons);
|
||||
collapsedContent.push(
|
||||
<div key='buttons' className='column-header__advanced-buttons'>
|
||||
{pinButton}
|
||||
{moveButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||
@ -178,7 +185,7 @@ class ColumnHeader extends PureComponent {
|
||||
onClick={this.handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={TuneIcon} />
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
@ -191,16 +198,19 @@ class ColumnHeader extends PureComponent {
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
||||
<>
|
||||
{showBackButton && backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||
{title}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasTitle && backButton}
|
||||
{!hasTitle && showBackButton && backButton}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{hasTitle && backButton}
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
|
@ -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 {
|
||||
}) : (
|
||||
<IconButton
|
||||
icon={!open ? icon : 'close'}
|
||||
iconComponent={!open ? iconComponent : CloseIcon}
|
||||
iconComponent={iconComponent}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
|
@ -5,10 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
|
||||
@ -68,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
||||
return (
|
||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||
<button className='dropdown-menu__text-button'>
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
@ -1,14 +1,12 @@
|
||||
import logo from '@/images/logo.svg';
|
||||
|
||||
export const WordmarkLogo = () => (
|
||||
export const WordmarkLogo: React.FC = () => (
|
||||
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
|
||||
<title>Mastodon</title>
|
||||
<use xlinkHref='#logo-symbol-wordmark' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SymbolLogo = () => (
|
||||
export const SymbolLogo: React.FC = () => (
|
||||
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
||||
);
|
||||
|
||||
export default WordmarkLogo;
|
@ -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 = () => (
|
||||
<div className='regeneration-indicator'>
|
||||
|
@ -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<IntlShape, 'formatDate' | 'formatMessage'>,
|
||||
date: Date,
|
||||
now: number,
|
||||
year: number,
|
||||
|
@ -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() {
|
||||
|
@ -381,7 +381,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
<div className='status__action-bar-spacer' />
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -80,7 +80,7 @@ export default class MediaContainer extends PureComponent {
|
||||
return (
|
||||
<IntlProvider>
|
||||
<>
|
||||
{[].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'));
|
||||
|
@ -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 {
|
||||
<ol className='rules-list'>
|
||||
{server.get('rules').map(rule => (
|
||||
<li key={rule.get('id')}>
|
||||
<span className='rules-list__text'>{rule.get('text')}</span>
|
||||
<div className='rules-list__text'>{rule.get('text')}</div>
|
||||
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@ -189,6 +189,7 @@ class About extends PureComponent {
|
||||
<>
|
||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||
|
||||
{domainBlocks.get('items').size > 0 && (
|
||||
<div className='about__domain-blocks'>
|
||||
{domainBlocks.get('items').map(block => (
|
||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||
@ -201,6 +202,7 @@ class About extends PureComponent {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<button className={classNames('account__domain-pill', { active: open })} ref={triggerRef} onClick={handleClick}>{domain}</button>
|
||||
|
||||
<Overlay show={open} rootClose onHide={handleClick} offset={[5, 5]} target={triggerRef}>
|
||||
{({ props }) => (
|
||||
<div {...props} className='account__domain-pill__popout dropdown-animation'>
|
||||
<div className='account__domain-pill__popout__header'>
|
||||
<div className='account__domain-pill__popout__header__icon'><Icon icon={BadgeIcon} /></div>
|
||||
<h3><FormattedMessage id='domain_pill.whats_in_a_handle' defaultMessage="What's in a handle?" /></h3>
|
||||
</div>
|
||||
|
||||
<div className='account__domain-pill__popout__handle'>
|
||||
<div className='account__domain-pill__popout__handle__label'>{isSelf ? <FormattedMessage id='domain_pill.your_handle' defaultMessage='Your handle:' /> : <FormattedMessage id='domain_pill.their_handle' defaultMessage='Their handle:' />}</div>
|
||||
<div className='account__domain-pill__popout__handle__handle'>@{username}@{domain}</div>
|
||||
</div>
|
||||
|
||||
<div className='account__domain-pill__popout__parts'>
|
||||
<div>
|
||||
<div className='account__domain-pill__popout__parts__icon'><Icon icon={AlternateEmailIcon} /></div>
|
||||
|
||||
<div>
|
||||
<h6><FormattedMessage id='domain_pill.username' defaultMessage='Username' /></h6>
|
||||
<p>{isSelf ? <FormattedMessage id='domain_pill.your_username' defaultMessage='Your unique identifier on this server. It’s possible to find users with the same username on different servers.' /> : <FormattedMessage id='domain_pill.their_username' defaultMessage='Their unique identifier on their server. It’s possible to find users with the same username on different servers.' />}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='account__domain-pill__popout__parts__icon'><Icon icon={GlobeIcon} /></div>
|
||||
|
||||
<div>
|
||||
<h6><FormattedMessage id='domain_pill.server' defaultMessage='Server' /></h6>
|
||||
<p>{isSelf ? <FormattedMessage id='domain_pill.your_server' defaultMessage='Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.' /> : <FormattedMessage id='domain_pill.their_server' defaultMessage='Their digital home, where all of their posts live.' />}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{isSelf ? <FormattedMessage id='domain_pill.who_you_are' defaultMessage='Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} /> : <FormattedMessage id='domain_pill.who_they_are' defaultMessage='Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} />}</p>
|
||||
|
||||
{expanded && (
|
||||
<>
|
||||
<p><FormattedMessage id='domain_pill.activitypub_like_language' defaultMessage='ActivityPub is like the language Mastodon speaks with other social networks.' /></p>
|
||||
<p><FormattedMessage id='domain_pill.activitypub_lets_connect' defaultMessage='It lets you connect and interact with people not just on Mastodon, but across different social apps too.' /></p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DomainPill.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
isSelf: PropTypes.bool,
|
||||
};
|
@ -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 = <Button disabled><LoadingIndicator /></Button>;
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} 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 {
|
||||
</a>
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
{shareBtn}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hidden && bellBtn}
|
||||
{!hidden && shareBtn}
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
{!hidden && actionBtn}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -364,7 +362,9 @@ class Header extends ImmutablePureComponent {
|
||||
<h1>
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
||||
<small>
|
||||
<span>@{acct}</span> {lockedIcon}
|
||||
<span>@{username}<span className='invisible'>@{domain}</span></span>
|
||||
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
|
||||
{lockedIcon}
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
@ -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 = {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user