mirror of
https://github.com/funamitech/mastodon
synced 2024-11-27 06:18:53 +09:00
Merge branch 'main' of https://github.com/glitch-soc/mastodon
This commit is contained in:
commit
8f563dd680
@ -73,6 +73,16 @@ DB_PORT=5432
|
|||||||
SECRET_KEY_BASE=
|
SECRET_KEY_BASE=
|
||||||
OTP_SECRET=
|
OTP_SECRET=
|
||||||
|
|
||||||
|
# Encryption secrets
|
||||||
|
# ------------------
|
||||||
|
# Must be available (and set to same values) for all server processes
|
||||||
|
# These are private/secret values, do not share outside hosting environment
|
||||||
|
# Use `bin/rails db:encryption:init` to generate fresh secrets
|
||||||
|
# Do not change these secrets once in use, as this would cause data loss and other issues
|
||||||
|
# ------------------
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
|
||||||
|
|
||||||
# Web Push
|
# Web Push
|
||||||
# --------
|
# --------
|
||||||
|
13
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
@ -1,6 +1,7 @@
|
|||||||
name: Bug Report (Web Interface)
|
name: Bug Report (Web Interface)
|
||||||
description: If you are using Mastodon's web interface and something is not working as expected
|
description: There is a problem using Mastodon's web interface.
|
||||||
labels: [bug, 'status/to triage', 'area/web interface']
|
labels: ['status/to triage', 'area/web interface']
|
||||||
|
type: Bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@ -47,8 +48,8 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Mastodon version
|
label: Mastodon version
|
||||||
description: |
|
description: |
|
||||||
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627`
|
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||||
placeholder: v4.1.2
|
placeholder: v4.3.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@ -56,7 +57,7 @@ body:
|
|||||||
label: Browser name and version
|
label: Browser name and version
|
||||||
description: |
|
description: |
|
||||||
What browser are you using when getting this bug? Please specify the version as well.
|
What browser are you using when getting this bug? Please specify the version as well.
|
||||||
placeholder: Firefox 105.0.3
|
placeholder: Firefox 131.0.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@ -64,7 +65,7 @@ body:
|
|||||||
label: Operating system
|
label: Operating system
|
||||||
description: |
|
description: |
|
||||||
What OS are you running? Please specify the version as well.
|
What OS are you running? Please specify the version as well.
|
||||||
placeholder: macOS 13.4.1
|
placeholder: macOS 15.0.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
13
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
@ -1,7 +1,8 @@
|
|||||||
name: Bug Report (server / API)
|
name: Bug Report (server / API)
|
||||||
description: |
|
description: |
|
||||||
If something is not working as expected, but is not from using the web interface.
|
There is a problem with the HTTP server, REST API, ActivityPub interaction, etc.
|
||||||
labels: [bug, 'status/to triage']
|
labels: ['status/to triage']
|
||||||
|
type: 'Bug'
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@ -48,8 +49,8 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Mastodon version
|
label: Mastodon version
|
||||||
description: |
|
description: |
|
||||||
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627`
|
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||||
placeholder: v4.1.2
|
placeholder: v4.3.0
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@ -59,7 +60,7 @@ body:
|
|||||||
Any additional technical details you may have, like logs or error traces
|
Any additional technical details you may have, like logs or error traces
|
||||||
value: |
|
value: |
|
||||||
If this is happening on your own Mastodon server, please fill out those:
|
If this is happening on your own Mastodon server, please fill out those:
|
||||||
- Ruby version: (from `ruby --version`, eg. v3.1.2)
|
- Ruby version: (from `ruby --version`, eg. v3.3.5)
|
||||||
- Node.js version: (from `node --version`, eg. v18.16.0)
|
- Node.js version: (from `node --version`, eg. v20.18.0)
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
74
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
name: Deployment troubleshooting
|
||||||
|
description: |
|
||||||
|
You are a server administrator and you are encountering a technical issue during installation, upgrade or operations of Mastodon.
|
||||||
|
labels: ['status/to triage']
|
||||||
|
type: 'Troubleshooting'
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||||
|
|
||||||
|
Please use a concise and distinct title for the issue.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the problem
|
||||||
|
description: What were you trying to do?
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Expected behaviour
|
||||||
|
description: What should have happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Actual behaviour
|
||||||
|
description: What happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Detailed description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mastodon instance
|
||||||
|
description: The address of the Mastodon instance where you experienced the issue
|
||||||
|
placeholder: mastodon.social
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mastodon version
|
||||||
|
description: |
|
||||||
|
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||||
|
placeholder: v4.3.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: |
|
||||||
|
Details about your environment, like how Mastodon is deployed, if containers are used, version numbers, etc.
|
||||||
|
value: |
|
||||||
|
Please at least include those informations:
|
||||||
|
- Operating system: (eg. Ubuntu 22.04)
|
||||||
|
- Ruby version: (from `ruby --version`, eg. v3.3.5)
|
||||||
|
- Node.js version: (from `node --version`, eg. v20.18.0)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Technical details
|
||||||
|
description: |
|
||||||
|
Any additional technical details you may have, like logs or error traces
|
||||||
|
validations:
|
||||||
|
required: false
|
@ -1,6 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: I have a suggestion
|
description: I have a suggestion
|
||||||
labels: [suggestion]
|
type: Suggestion
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@ -32,6 +32,8 @@ jobs:
|
|||||||
postgres:
|
postgres:
|
||||||
- 14-alpine
|
- 14-alpine
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
|
- 16-alpine
|
||||||
|
- 17-alpine
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
12
.github/workflows/test-ruby.yml
vendored
12
.github/workflows/test-ruby.yml
vendored
@ -124,7 +124,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.1'
|
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
@ -143,7 +142,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
additional-system-dependencies: ffmpeg libpam-dev
|
additional-system-dependencies: ffmpeg imagemagick libpam-dev
|
||||||
|
|
||||||
- name: Load database schema
|
- name: Load database schema
|
||||||
run: |
|
run: |
|
||||||
@ -226,7 +225,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.1'
|
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
@ -245,7 +243,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
additional-system-dependencies: ffmpeg libpam-dev libyaml-dev
|
additional-system-dependencies: ffmpeg libpam-dev
|
||||||
|
|
||||||
- name: Load database schema
|
- name: Load database schema
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
@ -305,7 +303,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.1'
|
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
|
||||||
@ -325,7 +322,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
additional-system-dependencies: ffmpeg
|
additional-system-dependencies: ffmpeg imagemagick
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
@ -422,7 +419,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.1'
|
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
search-image:
|
search-image:
|
||||||
@ -445,7 +441,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
additional-system-dependencies: ffmpeg
|
additional-system-dependencies: ffmpeg imagemagick
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
@ -8,7 +8,7 @@ AllCops:
|
|||||||
- lib/mastodon/migration_helpers.rb
|
- lib/mastodon/migration_helpers.rb
|
||||||
ExtraDetails: true
|
ExtraDetails: true
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
TargetRubyVersion: 3.1 # Oldest supported ruby version
|
TargetRubyVersion: 3.2 # Oldest supported ruby version
|
||||||
|
|
||||||
inherit_from:
|
inherit_from:
|
||||||
- .rubocop/layout.yml
|
- .rubocop/layout.yml
|
||||||
|
@ -1 +1 @@
|
|||||||
3.3.5
|
3.3.6
|
||||||
|
@ -67,7 +67,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
```html
|
```html
|
||||||
<meta name="fediverse:creator" content="username@domain" />
|
<meta name="fediverse:creator" content="username@domain" />
|
||||||
```
|
```
|
||||||
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\
|
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors \
|
||||||
Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\
|
Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\
|
||||||
This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1
|
This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1
|
||||||
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
|
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
|
||||||
|
@ -12,10 +12,10 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
|
|||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
|
||||||
# renovate: datasource=docker depName=docker.io/ruby
|
# renovate: datasource=docker depName=docker.io/ruby
|
||||||
ARG RUBY_VERSION="3.3.5"
|
ARG RUBY_VERSION="3.3.6"
|
||||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="20"
|
ARG NODE_MAJOR_VERSION="22"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||||
@ -191,7 +191,7 @@ FROM build AS libvips
|
|||||||
|
|
||||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||||
ARG VIPS_VERSION=8.15.3
|
ARG VIPS_VERSION=8.16.0
|
||||||
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
||||||
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
||||||
|
|
||||||
|
22
Gemfile
22
Gemfile
@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 3.1.0'
|
ruby '>= 3.2.0'
|
||||||
|
|
||||||
gem 'propshaft'
|
gem 'propshaft'
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rack', '~> 2.2.7'
|
gem 'rack', '~> 2.2.7'
|
||||||
gem 'rails', '~> 7.1.1'
|
gem 'rails', '~> 7.2.0'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
|
|
||||||
gem 'dotenv'
|
gem 'dotenv'
|
||||||
@ -16,16 +16,16 @@ gem 'pghero'
|
|||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
gem 'fog-core', '<= 2.5.0'
|
gem 'fog-core', '<= 2.6.0'
|
||||||
gem 'fog-openstack', '~> 1.0', require: false
|
gem 'fog-openstack', '~> 1.0', require: false
|
||||||
|
gem 'jd-paperclip-azure', '~> 3.0', require: false
|
||||||
gem 'kt-paperclip', '~> 7.2'
|
gem 'kt-paperclip', '~> 7.2'
|
||||||
gem 'md-paperclip-azure', '~> 2.2', require: false
|
|
||||||
gem 'ruby-vips', '~> 2.2', require: false
|
gem 'ruby-vips', '~> 2.2', require: false
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.18.0', require: false
|
gem 'bootsnap', '~> 1.18.0', require: false
|
||||||
gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.3'
|
gem 'chewy', '~> 7.3'
|
||||||
gem 'devise', '~> 4.9'
|
gem 'devise', '~> 4.9'
|
||||||
@ -47,13 +47,14 @@ gem 'color_diff', '~> 0.1'
|
|||||||
gem 'csv', '~> 3.2'
|
gem 'csv', '~> 3.2'
|
||||||
gem 'discard', '~> 1.2'
|
gem 'discard', '~> 1.2'
|
||||||
gem 'doorkeeper', '~> 5.6'
|
gem 'doorkeeper', '~> 5.6'
|
||||||
|
gem 'faraday-httpclient'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 5.2.0'
|
gem 'http', '~> 5.2.0'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 1.7.0'
|
gem 'httplog', '~> 1.7.0', require: false
|
||||||
gem 'i18n'
|
gem 'i18n'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'inline_svg'
|
gem 'inline_svg'
|
||||||
@ -61,7 +62,8 @@ gem 'irb', '~> 1.8'
|
|||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
||||||
|
gem 'mutex_m'
|
||||||
gem 'nokogiri', '~> 1.15'
|
gem 'nokogiri', '~> 1.15'
|
||||||
gem 'oj', '~> 3.14'
|
gem 'oj', '~> 3.14'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
@ -111,8 +113,8 @@ group :opentelemetry do
|
|||||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
|
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||||
@ -220,7 +222,7 @@ gem 'concurrent-ruby', require: false
|
|||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
gem 'net-http', '~> 0.4.0'
|
gem 'net-http', '~> 0.5.0'
|
||||||
gem 'rubyzip', '~> 2.3'
|
gem 'rubyzip', '~> 2.3'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
302
Gemfile.lock
302
Gemfile.lock
@ -10,51 +10,46 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.4)
|
actioncable (7.2.2)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.4)
|
actionmailbox (7.2.2)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.2.2)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.2.2)
|
||||||
activestorage (= 7.1.4)
|
activestorage (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.8.0)
|
||||||
net-imap
|
actionmailer (7.2.2)
|
||||||
net-pop
|
actionpack (= 7.2.2)
|
||||||
net-smtp
|
actionview (= 7.2.2)
|
||||||
actionmailer (7.1.4)
|
activejob (= 7.2.2)
|
||||||
actionpack (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
actionview (= 7.1.4)
|
mail (>= 2.8.0)
|
||||||
activejob (= 7.1.4)
|
|
||||||
activesupport (= 7.1.4)
|
|
||||||
mail (~> 2.5, >= 2.5.4)
|
|
||||||
net-imap
|
|
||||||
net-pop
|
|
||||||
net-smtp
|
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.4)
|
actionpack (7.2.2)
|
||||||
actionview (= 7.1.4)
|
actionview (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4, < 3.2)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.4)
|
useragent (~> 0.16)
|
||||||
actionpack (= 7.1.4)
|
actiontext (7.2.2)
|
||||||
activerecord (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
activestorage (= 7.1.4)
|
activerecord (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activestorage (= 7.2.2)
|
||||||
|
activesupport (= 7.2.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.4)
|
actionview (7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
@ -64,31 +59,33 @@ GEM
|
|||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.1.4)
|
activejob (7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.4)
|
activemodel (7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
activerecord (7.1.4)
|
activerecord (7.2.2)
|
||||||
activemodel (= 7.1.4)
|
activemodel (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.4)
|
activestorage (7.2.2)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.2.2)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.4)
|
activesupport (7.2.2)
|
||||||
base64
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
@ -100,32 +97,27 @@ GEM
|
|||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.983.0)
|
aws-partitions (1.1001.0)
|
||||||
aws-sdk-core (3.209.1)
|
aws-sdk-core (3.212.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.94.0)
|
aws-sdk-kms (1.95.0)
|
||||||
aws-sdk-core (~> 3, >= 3.207.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.167.0)
|
aws-sdk-s3 (1.170.0)
|
||||||
aws-sdk-core (~> 3, >= 3.207.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.0)
|
aws-sigv4 (1.10.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-blob (0.5.2)
|
||||||
azure-storage-common (~> 2.0)
|
rexml
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
|
||||||
azure-storage-common (2.0.4)
|
|
||||||
faraday (~> 1.0)
|
|
||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
|
||||||
net-http-persistent (~> 4.0)
|
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
|
benchmark (0.3.0)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
@ -137,9 +129,9 @@ GEM
|
|||||||
blurhash (0.1.8)
|
blurhash (0.1.8)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.18.4)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (6.2.1)
|
brakeman (6.2.2)
|
||||||
racc
|
racc
|
||||||
browser (5.3.1)
|
browser (6.0.0)
|
||||||
brpoplpush-redis_script (0.1.3)
|
brpoplpush-redis_script (0.1.3)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
redis (>= 1.0, < 6)
|
redis (>= 1.0, < 6)
|
||||||
@ -179,7 +171,7 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.19.0)
|
css_parser (1.19.1)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.0)
|
csv (3.3.0)
|
||||||
database_cleaner-active_record (2.2.0)
|
database_cleaner-active_record (2.2.0)
|
||||||
@ -206,8 +198,8 @@ GEM
|
|||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.5.1)
|
||||||
discard (1.3.0)
|
discard (1.4.0)
|
||||||
activerecord (>= 4.2, < 8)
|
activerecord (>= 4.2, < 9.0)
|
||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
domain_name (0.6.20240107)
|
domain_name (0.6.20240107)
|
||||||
doorkeeper (5.7.1)
|
doorkeeper (5.7.1)
|
||||||
@ -231,38 +223,21 @@ GEM
|
|||||||
erubi (1.13.0)
|
erubi (1.13.0)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.111.0)
|
excon (0.112.0)
|
||||||
fabrication (2.31.0)
|
fabrication (2.31.0)
|
||||||
faker (3.4.2)
|
faker (3.5.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (2.12.0)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-net_http (>= 2.0, < 3.4)
|
||||||
faraday-em_synchrony (~> 1.0)
|
json
|
||||||
faraday-excon (~> 1.1)
|
logger
|
||||||
faraday-httpclient (~> 1.0)
|
faraday-httpclient (2.0.1)
|
||||||
faraday-multipart (~> 1.0)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (3.3.0)
|
||||||
faraday-net_http_persistent (~> 1.0)
|
net-http
|
||||||
faraday-patron (~> 1.0)
|
|
||||||
faraday-rack (~> 1.0)
|
|
||||||
faraday-retry (~> 1.0)
|
|
||||||
ruby2_keywords (>= 0.0.4)
|
|
||||||
faraday-em_http (1.0.0)
|
|
||||||
faraday-em_synchrony (1.0.0)
|
|
||||||
faraday-excon (1.1.0)
|
|
||||||
faraday-httpclient (1.0.1)
|
|
||||||
faraday-multipart (1.0.4)
|
|
||||||
multipart-post (~> 2)
|
|
||||||
faraday-net_http (1.0.2)
|
|
||||||
faraday-net_http_persistent (1.2.0)
|
|
||||||
faraday-patron (1.0.0)
|
|
||||||
faraday-rack (1.0.0)
|
|
||||||
faraday-retry (1.0.3)
|
|
||||||
faraday_middleware (1.2.0)
|
|
||||||
faraday (~> 1.0)
|
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.3.1)
|
fastimage (2.3.1)
|
||||||
ffi (1.16.3)
|
ffi (1.17.0)
|
||||||
ffi-compiler (1.3.2)
|
ffi-compiler (1.3.2)
|
||||||
ffi (>= 1.15.5)
|
ffi (>= 1.15.5)
|
||||||
rake
|
rake
|
||||||
@ -350,8 +325,12 @@ GEM
|
|||||||
irb (1.14.1)
|
irb (1.14.1)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
|
jd-paperclip-azure (3.0.0)
|
||||||
|
addressable (~> 2.5)
|
||||||
|
azure-blob (~> 0.5.2)
|
||||||
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.2)
|
json (2.7.4)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.15.3.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
@ -366,7 +345,7 @@ GEM
|
|||||||
rack (>= 2.2, < 4)
|
rack (>= 2.2, < 4)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rexml (~> 3.2)
|
rexml (~> 3.2)
|
||||||
json-ld-preloaded (3.3.0)
|
json-ld-preloaded (3.3.1)
|
||||||
json-ld (~> 3.3)
|
json-ld (~> 3.3)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
json-schema (5.0.1)
|
json-schema (5.0.1)
|
||||||
@ -412,7 +391,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.22.0)
|
loofah (2.23.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@ -424,26 +403,20 @@ GEM
|
|||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
md-paperclip-azure (2.2.0)
|
|
||||||
addressable (~> 2.5)
|
|
||||||
azure-storage-blob (~> 2.0.1)
|
|
||||||
hashie (~> 5.0)
|
|
||||||
memory_profiler (1.1.0)
|
memory_profiler (1.1.0)
|
||||||
mime-types (3.5.2)
|
mime-types (3.6.0)
|
||||||
|
logger
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2024.0820)
|
mime-types-data (3.2024.1001)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.25.1)
|
minitest (5.25.1)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.3)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.1)
|
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
net-http (0.4.1)
|
net-http (0.5.0)
|
||||||
uri
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-imap (0.5.0)
|
||||||
connection_pool (~> 2.2)
|
|
||||||
net-imap (0.4.15)
|
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.19.0)
|
net-ldap (0.19.0)
|
||||||
@ -457,7 +430,7 @@ GEM
|
|||||||
nokogiri (1.16.7)
|
nokogiri (1.16.7)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.6)
|
oj (3.16.7)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.2)
|
||||||
@ -503,7 +476,7 @@ GEM
|
|||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_mailer (0.1.0)
|
opentelemetry-instrumentation-action_mailer (0.2.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
@ -515,13 +488,13 @@ GEM
|
|||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_job (0.7.7)
|
opentelemetry-instrumentation-active_job (0.7.8)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_record (0.7.3)
|
opentelemetry-instrumentation-active_record (0.8.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_support (0.6.0)
|
opentelemetry-instrumentation-active_support (0.6.0)
|
||||||
@ -553,16 +526,16 @@ GEM
|
|||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rack (0.24.6)
|
opentelemetry-instrumentation-rack (0.25.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rails (0.31.2)
|
opentelemetry-instrumentation-rails (0.32.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
opentelemetry-instrumentation-active_record (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-redis (0.25.7)
|
opentelemetry-instrumentation-redis (0.25.7)
|
||||||
@ -590,8 +563,8 @@ GEM
|
|||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.8)
|
pg (1.5.9)
|
||||||
pghero (3.6.0)
|
pghero (3.6.1)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
addressable
|
addressable
|
||||||
@ -638,20 +611,20 @@ GEM
|
|||||||
rackup (1.0.0)
|
rackup (1.0.0)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
webrick
|
webrick
|
||||||
rails (7.1.4)
|
rails (7.2.2)
|
||||||
actioncable (= 7.1.4)
|
actioncable (= 7.2.2)
|
||||||
actionmailbox (= 7.1.4)
|
actionmailbox (= 7.2.2)
|
||||||
actionmailer (= 7.1.4)
|
actionmailer (= 7.2.2)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
actiontext (= 7.1.4)
|
actiontext (= 7.2.2)
|
||||||
actionview (= 7.1.4)
|
actionview (= 7.2.2)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.2.2)
|
||||||
activemodel (= 7.1.4)
|
activemodel (= 7.2.2)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.2.2)
|
||||||
activestorage (= 7.1.4)
|
activestorage (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.4)
|
railties (= 7.2.2)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
@ -663,13 +636,13 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.9)
|
rails-i18n (7.0.10)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.1.4)
|
railties (7.2.2)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.2.2)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.2.2)
|
||||||
irb
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
@ -698,9 +671,9 @@ GEM
|
|||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.8)
|
rexml (3.3.9)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.3.0)
|
rouge (4.4.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@ -710,14 +683,14 @@ GEM
|
|||||||
rspec-core (~> 3.13.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (~> 3.13.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
rspec-mocks (~> 3.13.0)
|
rspec-mocks (~> 3.13.0)
|
||||||
rspec-core (3.13.1)
|
rspec-core (3.13.2)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.2)
|
rspec-expectations (3.13.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-github (2.4.0)
|
rspec-github (2.4.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-mocks (3.13.1)
|
rspec-mocks (3.13.2)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (7.0.1)
|
rspec-rails (7.0.1)
|
||||||
@ -751,17 +724,17 @@ GEM
|
|||||||
rubocop-performance (1.22.1)
|
rubocop-performance (1.22.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.26.2)
|
rubocop-rails (2.27.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (3.1.0)
|
rubocop-rspec (3.2.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec_rails (2.30.0)
|
rubocop-rspec_rails (2.30.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec (~> 3, >= 3.0.1)
|
rubocop-rspec (~> 3, >= 3.0.1)
|
||||||
ruby-prof (1.7.0)
|
ruby-prof (1.7.1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-saml (1.17.0)
|
ruby-saml (1.17.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
@ -769,7 +742,6 @@ GEM
|
|||||||
ruby-vips (2.2.2)
|
ruby-vips (2.2.2)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
rufus-scheduler (3.9.1)
|
rufus-scheduler (3.9.1)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
@ -781,7 +753,8 @@ GEM
|
|||||||
scenic (1.8.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
selenium-webdriver (4.25.0)
|
securerandom (0.3.1)
|
||||||
|
selenium-webdriver (4.26.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
@ -822,7 +795,7 @@ GEM
|
|||||||
stoplight (4.1.0)
|
stoplight (4.1.0)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.1)
|
stringio (3.1.1)
|
||||||
strong_migrations (2.0.0)
|
strong_migrations (2.0.2)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
@ -864,6 +837,7 @@ GEM
|
|||||||
unf_ext (0.0.9.1)
|
unf_ext (0.0.9.1)
|
||||||
unicode-display_width (2.6.0)
|
unicode-display_width (2.6.0)
|
||||||
uri (0.13.1)
|
uri (0.13.1)
|
||||||
|
useragent (0.16.10)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
@ -902,7 +876,7 @@ GEM
|
|||||||
xorcist (1.1.3)
|
xorcist (1.1.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.18)
|
zeitwerk (2.7.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -917,7 +891,7 @@ DEPENDENCIES
|
|||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.18.0)
|
bootsnap (~> 1.18.0)
|
||||||
brakeman (~> 6.0)
|
brakeman (~> 6.0)
|
||||||
browser (< 6)
|
browser
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capybara (~> 3.39)
|
capybara (~> 3.39)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
@ -939,10 +913,11 @@ DEPENDENCIES
|
|||||||
email_spec
|
email_spec
|
||||||
fabrication (~> 2.30)
|
fabrication (~> 2.30)
|
||||||
faker (~> 3.2)
|
faker (~> 3.2)
|
||||||
|
faraday-httpclient
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
flatware-rspec
|
flatware-rspec
|
||||||
fog-core (<= 2.5.0)
|
fog-core (<= 2.6.0)
|
||||||
fog-openstack (~> 1.0)
|
fog-openstack (~> 1.0)
|
||||||
haml-rails (~> 2.0)
|
haml-rails (~> 2.0)
|
||||||
haml_lint
|
haml_lint
|
||||||
@ -957,6 +932,7 @@ DEPENDENCIES
|
|||||||
idn-ruby
|
idn-ruby
|
||||||
inline_svg
|
inline_svg
|
||||||
irb (~> 1.8)
|
irb (~> 1.8)
|
||||||
|
jd-paperclip-azure (~> 3.0)
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
json-schema (~> 5.0)
|
json-schema (~> 5.0)
|
||||||
@ -968,10 +944,10 @@ DEPENDENCIES
|
|||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
mail (~> 2.8)
|
mail (~> 2.8)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
md-paperclip-azure (~> 2.2)
|
|
||||||
memory_profiler
|
memory_profiler
|
||||||
mime-types (~> 3.5.0)
|
mime-types (~> 3.6.0)
|
||||||
net-http (~> 0.4.0)
|
mutex_m
|
||||||
|
net-http (~> 0.5.0)
|
||||||
net-ldap (~> 0.18)
|
net-ldap (~> 0.18)
|
||||||
nokogiri (~> 1.15)
|
nokogiri (~> 1.15)
|
||||||
oj (~> 3.14)
|
oj (~> 3.14)
|
||||||
@ -991,8 +967,8 @@ DEPENDENCIES
|
|||||||
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
||||||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.24.1)
|
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||||
opentelemetry-instrumentation-rails (~> 0.31.0)
|
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||||
opentelemetry-sdk (~> 1.4)
|
opentelemetry-sdk (~> 1.4)
|
||||||
@ -1009,7 +985,7 @@ DEPENDENCIES
|
|||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors (~> 2.0)
|
rack-cors (~> 2.0)
|
||||||
rack-test (~> 2.1)
|
rack-test (~> 2.1)
|
||||||
rails (~> 7.1.1)
|
rails (~> 7.2.0)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 7.0)
|
rails-i18n (~> 7.0)
|
||||||
rdf-normalize (~> 0.5)
|
rdf-normalize (~> 0.5)
|
||||||
@ -1057,7 +1033,7 @@ DEPENDENCIES
|
|||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.3.4p94
|
ruby 3.3.5p100
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.18
|
2.5.22
|
||||||
|
@ -97,7 +97,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
|
|||||||
|
|
||||||
- **PostgreSQL** 12+
|
- **PostgreSQL** 12+
|
||||||
- **Redis** 4+
|
- **Redis** 4+
|
||||||
- **Ruby** 3.1+
|
- **Ruby** 3.2+
|
||||||
- **Node.js** 18+
|
- **Node.js** 18+
|
||||||
|
|
||||||
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. 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.
|
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. 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.
|
||||||
|
2
Rakefile
2
Rakefile
@ -3,6 +3,6 @@
|
|||||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||||
|
|
||||||
require File.expand_path('config/application', __dir__)
|
require_relative 'config/application'
|
||||||
|
|
||||||
Rails.application.load_tasks
|
Rails.application.load_tasks
|
||||||
|
@ -41,11 +41,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def outbox_url(**kwargs)
|
def outbox_url(**)
|
||||||
if params[:account_username].present?
|
if params[:account_username].present?
|
||||||
account_outbox_url(@account, **kwargs)
|
account_outbox_url(@account, **)
|
||||||
else
|
else
|
||||||
instance_actor_outbox_url(**kwargs)
|
instance_actor_outbox_url(**)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ module Admin
|
|||||||
def index
|
def index
|
||||||
authorize :email_domain_block, :index?
|
authorize :email_domain_block, :index?
|
||||||
|
|
||||||
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
|
@email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page])
|
||||||
@form = Form::EmailDomainBlockBatch.new
|
@form = Form::EmailDomainBlockBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ module Admin
|
|||||||
|
|
||||||
def deactivate_all
|
def deactivate_all
|
||||||
authorize :invite, :deactivate_all?
|
authorize :invite, :deactivate_all?
|
||||||
Invite.available.in_batches.update_all(expires_at: Time.now.utc)
|
Invite.available.in_batches.touch_all(:expires_at)
|
||||||
redirect_to admin_invites_path
|
redirect_to admin_invites_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController
|
|||||||
def index
|
def index
|
||||||
authorize :preview_card, :review?
|
authorize :preview_card, :review?
|
||||||
|
|
||||||
@locales = PreviewCardTrend.pluck('distinct language')
|
@locales = PreviewCardTrend.locales
|
||||||
@preview_cards = filtered_preview_cards.page(params[:page])
|
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||||
@form = Trends::PreviewCardBatch.new
|
@form = Trends::PreviewCardBatch.new
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController
|
|||||||
def index
|
def index
|
||||||
authorize [:admin, :status], :review?
|
authorize [:admin, :status], :review?
|
||||||
|
|
||||||
@locales = StatusTrend.pluck('distinct language')
|
@locales = StatusTrend.locales
|
||||||
@statuses = filtered_statuses.page(params[:page])
|
@statuses = filtered_statuses.page(params[:page])
|
||||||
@form = Trends::StatusBatch.new
|
@form = Trends::StatusBatch.new
|
||||||
end
|
end
|
||||||
|
@ -106,8 +106,8 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id
|
render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
def relationships(**)
|
||||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([@account], current_user.account_id, **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_ids
|
def account_ids
|
||||||
|
@ -17,6 +17,17 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
|||||||
relationships: @relationships
|
relationships: @relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
with_read_replica do
|
||||||
|
@presenter = AnnualReportsPresenter.new([@annual_report])
|
||||||
|
@relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @presenter,
|
||||||
|
serializer: REST::AnnualReportsSerializer,
|
||||||
|
relationships: @relationships
|
||||||
|
end
|
||||||
|
|
||||||
def read
|
def read
|
||||||
@annual_report.view!
|
@annual_report.view!
|
||||||
render_empty
|
render_empty
|
||||||
|
@ -28,8 +28,8 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||||||
@account ||= Account.find(params[:id])
|
@account ||= Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
def relationships(**)
|
||||||
AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([account], current_user.account_id, **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_accounts
|
def load_accounts
|
||||||
|
@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_translation
|
def set_translation
|
||||||
@translation = TranslateStatusService.new.call(@status, content_locale)
|
@translation = TranslateStatusService.new.call(@status, I18n.locale.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!, except: :destroy
|
||||||
before_action :set_push_subscription, only: :update
|
before_action :set_push_subscription, only: :update
|
||||||
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
||||||
after_action :update_session_with_subscription, only: :create
|
after_action :update_session_with_subscription, only: :create
|
||||||
@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
|
||||||
|
push_subscription&.destroy
|
||||||
|
|
||||||
|
head 200
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def active_session
|
def active_session
|
||||||
|
@ -10,7 +10,7 @@ module Auth::CaptchaConcern
|
|||||||
end
|
end
|
||||||
|
|
||||||
def captcha_available?
|
def captcha_available?
|
||||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def captcha_enabled?
|
def captcha_enabled?
|
||||||
|
11
app/controllers/oauth/userinfo_controller.rb
Normal file
11
app/controllers/oauth/userinfo_controller.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Oauth::UserinfoController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def show
|
||||||
|
@account = current_account
|
||||||
|
render json: @account, serializer: OauthUserinfoSerializer
|
||||||
|
end
|
||||||
|
end
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Admin::SettingsHelper
|
module Admin::SettingsHelper
|
||||||
def captcha_available?
|
def captcha_available?
|
||||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_activity_title(activity)
|
def login_activity_title(activity)
|
||||||
|
@ -120,18 +120,6 @@ module ApplicationHelper
|
|||||||
inline_svg_tag 'check.svg'
|
inline_svg_tag 'check.svg'
|
||||||
end
|
end
|
||||||
|
|
||||||
def visibility_icon(status)
|
|
||||||
if status.public_visibility?
|
|
||||||
material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
|
|
||||||
elsif status.unlisted_visibility?
|
|
||||||
material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
|
|
||||||
elsif status.private_visibility? || status.limited_visibility?
|
|
||||||
material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
|
|
||||||
elsif status.direct_visibility?
|
|
||||||
material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def interrelationships_icon(relationships, account_id)
|
def interrelationships_icon(relationships, account_id)
|
||||||
if relationships.following[account_id] && relationships.followed_by[account_id]
|
if relationships.following[account_id] && relationships.followed_by[account_id]
|
||||||
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
||||||
@ -245,18 +233,23 @@ module ApplicationHelper
|
|||||||
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recent_tag_usage(tag)
|
||||||
|
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
|
||||||
|
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
|
||||||
|
end
|
||||||
|
|
||||||
# glitch-soc addition to handle the multiple flavors
|
# glitch-soc addition to handle the multiple flavors
|
||||||
def preload_locale_pack
|
def preload_locale_pack
|
||||||
supported_locales = Themes.instance.flavour(current_flavour)['locales']
|
supported_locales = Themes.instance.flavour(current_flavour)['locales']
|
||||||
preload_pack_asset "locales/#{current_flavour}/#{I18n.locale}-json.js" if supported_locales.include?(I18n.locale.to_s)
|
preload_pack_asset "locales/#{current_flavour}/#{I18n.locale}-json.js" if supported_locales.include?(I18n.locale.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def flavoured_javascript_pack_tag(pack_name, **options)
|
def flavoured_javascript_pack_tag(pack_name, **)
|
||||||
javascript_pack_tag("flavours/#{current_flavour}/#{pack_name}", **options)
|
javascript_pack_tag("flavours/#{current_flavour}/#{pack_name}", **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def flavoured_stylesheet_pack_tag(pack_name, **options)
|
def flavoured_stylesheet_pack_tag(pack_name, **)
|
||||||
stylesheet_pack_tag("flavours/#{current_flavour}/#{pack_name}", **options)
|
stylesheet_pack_tag("flavours/#{current_flavour}/#{pack_name}", **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def preload_signed_in_js_packs
|
def preload_signed_in_js_packs
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module FormattingHelper
|
module FormattingHelper
|
||||||
|
SYNDICATED_EMOJI_STYLES = <<~CSS.squish
|
||||||
|
height: 1.1em;
|
||||||
|
margin: -.2ex .15em .2ex;
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 1.1em;
|
||||||
|
CSS
|
||||||
|
|
||||||
def html_aware_format(text, local, options = {})
|
def html_aware_format(text, local, options = {})
|
||||||
HtmlAwareFormatter.new(text, local, options).to_s
|
HtmlAwareFormatter.new(text, local, options).to_s
|
||||||
end
|
end
|
||||||
@ -19,42 +27,33 @@ module FormattingHelper
|
|||||||
module_function :extract_status_plain_text
|
module_function :extract_status_plain_text
|
||||||
|
|
||||||
def status_content_format(status)
|
def status_content_format(status)
|
||||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span|
|
||||||
|
span.add_attributes(
|
||||||
|
'app.formatter.content.type' => 'status',
|
||||||
|
'app.formatter.content.origin' => status.local? ? 'local' : 'remote'
|
||||||
|
)
|
||||||
|
|
||||||
|
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rss_status_content_format(status)
|
def rss_status_content_format(status)
|
||||||
html = status_content_format(status)
|
|
||||||
|
|
||||||
before_html = if status.spoiler_text?
|
|
||||||
tag.p do
|
|
||||||
tag.strong do
|
|
||||||
I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)
|
|
||||||
end
|
|
||||||
|
|
||||||
status.spoiler_text
|
|
||||||
end + tag.hr
|
|
||||||
end
|
|
||||||
|
|
||||||
after_html = if status.preloadable_poll
|
|
||||||
tag.p do
|
|
||||||
safe_join(
|
|
||||||
status.preloadable_poll.options.map do |o|
|
|
||||||
tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true)
|
|
||||||
end,
|
|
||||||
tag.br
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
prerender_custom_emojis(
|
prerender_custom_emojis(
|
||||||
safe_join([before_html, html, after_html]),
|
wrapped_status_content_format(status),
|
||||||
status.emojis,
|
status.emojis,
|
||||||
style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex'
|
style: SYNDICATED_EMOJI_STYLES
|
||||||
).to_str
|
).to_str
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_bio_format(account)
|
def account_bio_format(account)
|
||||||
html_aware_format(account.note, account.local?)
|
MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span|
|
||||||
|
span.add_attributes(
|
||||||
|
'app.formatter.content.type' => 'account_bio',
|
||||||
|
'app.formatter.content.origin' => account.local? ? 'local' : 'remote'
|
||||||
|
)
|
||||||
|
|
||||||
|
html_aware_format(account.note, account.local?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_field_value_format(field, with_rel_me: true)
|
def account_field_value_format(field, with_rel_me: true)
|
||||||
@ -64,4 +63,47 @@ module FormattingHelper
|
|||||||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def wrapped_status_content_format(status)
|
||||||
|
safe_join [
|
||||||
|
rss_content_preroll(status),
|
||||||
|
status_content_format(status),
|
||||||
|
rss_content_postroll(status),
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def rss_content_preroll(status)
|
||||||
|
if status.spoiler_text?
|
||||||
|
safe_join [
|
||||||
|
tag.p { spoiler_with_warning(status) },
|
||||||
|
tag.hr,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def spoiler_with_warning(status)
|
||||||
|
safe_join [
|
||||||
|
tag.strong { I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) },
|
||||||
|
status.spoiler_text,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def rss_content_postroll(status)
|
||||||
|
if status.preloadable_poll
|
||||||
|
tag.p do
|
||||||
|
poll_option_tags(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def poll_option_tags(status)
|
||||||
|
safe_join(
|
||||||
|
status.preloadable_poll.options.map do |option|
|
||||||
|
tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', option, disabled: true)
|
||||||
|
end,
|
||||||
|
tag.br
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -162,7 +162,7 @@ module LanguagesHelper
|
|||||||
th: ['Thai', 'ไทย'].freeze,
|
th: ['Thai', 'ไทย'].freeze,
|
||||||
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
||||||
tk: ['Turkmen', 'Türkmen'].freeze,
|
tk: ['Turkmen', 'Türkmen'].freeze,
|
||||||
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
|
tl: ['Tagalog', 'Tagalog'].freeze,
|
||||||
tn: ['Tswana', 'Setswana'].freeze,
|
tn: ['Tswana', 'Setswana'].freeze,
|
||||||
to: ['Tonga', 'faka Tonga'].freeze,
|
to: ['Tonga', 'faka Tonga'].freeze,
|
||||||
tr: ['Turkish', 'Türkçe'].freeze,
|
tr: ['Turkish', 'Türkçe'].freeze,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module MediaComponentHelper
|
module MediaComponentHelper
|
||||||
def render_video_component(status, **options)
|
def render_video_component(status, **)
|
||||||
video = status.ordered_media_attachments.first
|
video = status.ordered_media_attachments.first
|
||||||
|
|
||||||
meta = video.file.meta || {}
|
meta = video.file.meta || {}
|
||||||
@ -18,14 +18,14 @@ module MediaComponentHelper
|
|||||||
media: [
|
media: [
|
||||||
serialize_media_attachment(video),
|
serialize_media_attachment(video),
|
||||||
].as_json,
|
].as_json,
|
||||||
}.merge(**options)
|
}.merge(**)
|
||||||
|
|
||||||
react_component :video, component_params do
|
react_component :video, component_params do
|
||||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_audio_component(status, **options)
|
def render_audio_component(status, **)
|
||||||
audio = status.ordered_media_attachments.first
|
audio = status.ordered_media_attachments.first
|
||||||
|
|
||||||
meta = audio.file.meta || {}
|
meta = audio.file.meta || {}
|
||||||
@ -38,19 +38,19 @@ module MediaComponentHelper
|
|||||||
foregroundColor: meta.dig('colors', 'foreground'),
|
foregroundColor: meta.dig('colors', 'foreground'),
|
||||||
accentColor: meta.dig('colors', 'accent'),
|
accentColor: meta.dig('colors', 'accent'),
|
||||||
duration: meta.dig('original', 'duration'),
|
duration: meta.dig('original', 'duration'),
|
||||||
}.merge(**options)
|
}.merge(**)
|
||||||
|
|
||||||
react_component :audio, component_params do
|
react_component :audio, component_params do
|
||||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_media_gallery_component(status, **options)
|
def render_media_gallery_component(status, **)
|
||||||
component_params = {
|
component_params = {
|
||||||
sensitive: sensitive_viewer?(status, current_account),
|
sensitive: sensitive_viewer?(status, current_account),
|
||||||
autoplay: prefers_autoplay?,
|
autoplay: prefers_autoplay?,
|
||||||
media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json },
|
media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json },
|
||||||
}.merge(**options)
|
}.merge(**)
|
||||||
|
|
||||||
react_component :media_gallery, component_params do
|
react_component :media_gallery, component_params do
|
||||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||||
|
@ -16,6 +16,6 @@ module RegistrationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def ip_blocked?(remote_ip)
|
def ip_blocked?(remote_ip)
|
||||||
IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s])
|
IpBlock.severity_sign_up_block.containing(remote_ip.to_s).exists?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,8 +14,8 @@ module RoutingHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_asset_url(source, **options)
|
def full_asset_url(source, **)
|
||||||
source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage?
|
source = ActionController::Base.helpers.asset_url(source, **) unless use_storage?
|
||||||
|
|
||||||
URI.join(asset_host, source).to_s
|
URI.join(asset_host, source).to_s
|
||||||
end
|
end
|
||||||
@ -24,12 +24,12 @@ module RoutingHelper
|
|||||||
Rails.configuration.action_controller.asset_host || root_url
|
Rails.configuration.action_controller.asset_host || root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_path(source, **options)
|
def frontend_asset_path(source, **)
|
||||||
asset_pack_path("media/#{source}", **options)
|
asset_pack_path("media/#{source}", **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_url(source, **options)
|
def frontend_asset_url(source, **)
|
||||||
full_asset_url(frontend_asset_path(source, **options))
|
full_asset_url(frontend_asset_path(source, **))
|
||||||
end
|
end
|
||||||
|
|
||||||
def use_storage?
|
def use_storage?
|
||||||
|
@ -12,7 +12,7 @@ module StatusesHelper
|
|||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def nothing_here(extra_classes = '')
|
def nothing_here(extra_classes = '')
|
||||||
content_tag(:div, class: "nothing-here #{extra_classes}") do
|
tag.div(class: ['nothing-here', extra_classes]) do
|
||||||
t('accounts.nothing_here')
|
t('accounts.nothing_here')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
|||||||
|
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const oldReadOnly = input.readOnly;
|
navigator.clipboard
|
||||||
|
.writeText(input.value)
|
||||||
input.readOnly = false;
|
.then(() => {
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
input.setSelectionRange(0, input.value.length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (document.execCommand('copy')) {
|
|
||||||
input.blur();
|
|
||||||
|
|
||||||
const parent = target.parentElement;
|
const parent = target.parentElement;
|
||||||
|
|
||||||
if (!parent) return;
|
if (parent) {
|
||||||
parent.classList.add('copied');
|
parent.classList.add('copied');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
parent.classList.remove('copied');
|
parent.classList.remove('copied');
|
||||||
}, 700);
|
}, 700);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.readOnly = oldReadOnly;
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
|
@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
|||||||
import type {
|
import type {
|
||||||
ApiNotificationGroupJSON,
|
ApiNotificationGroupJSON,
|
||||||
ApiNotificationJSON,
|
ApiNotificationJSON,
|
||||||
|
NotificationType,
|
||||||
} from 'flavours/glitch/api_types/notifications';
|
} from 'flavours/glitch/api_types/notifications';
|
||||||
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
|
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
|
||||||
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
||||||
@ -15,6 +16,7 @@ import { usePendingItems } from 'flavours/glitch/initial_state';
|
|||||||
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
|
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
|
||||||
import {
|
import {
|
||||||
selectSettingsNotificationsExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
selectSettingsNotificationsGroupFollows,
|
||||||
selectSettingsNotificationsQuickFilterActive,
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
selectSettingsNotificationsShows,
|
selectSettingsNotificationsShows,
|
||||||
} from 'flavours/glitch/selectors/settings';
|
} from 'flavours/glitch/selectors/settings';
|
||||||
@ -68,17 +70,19 @@ function dispatchAssociatedRecords(
|
|||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
function selectNotificationGroupedTypes(state: RootState) {
|
||||||
|
const types: NotificationType[] = ['favourite', 'reblog'];
|
||||||
|
|
||||||
export function shouldGroupNotificationType(type: string) {
|
if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
|
||||||
return supportedGroupedNotificationTypes.includes(type);
|
|
||||||
|
return types;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
'notificationGroups/fetch',
|
'notificationGroups/fetch',
|
||||||
async (_params, { getState }) =>
|
async (_params, { getState }) =>
|
||||||
apiFetchNotificationGroups({
|
apiFetchNotificationGroups({
|
||||||
grouped_types: supportedGroupedNotificationTypes,
|
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
}),
|
}),
|
||||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||||||
'notificationGroups/fetchGap',
|
'notificationGroups/fetchGap',
|
||||||
async (params: { gap: NotificationGap }, { getState }) =>
|
async (params: { gap: NotificationGap }, { getState }) =>
|
||||||
apiFetchNotificationGroups({
|
apiFetchNotificationGroups({
|
||||||
grouped_types: supportedGroupedNotificationTypes,
|
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||||
max_id: params.gap.maxId,
|
max_id: params.gap.maxId,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
}),
|
}),
|
||||||
@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||||||
'notificationGroups/pollRecentNotifications',
|
'notificationGroups/pollRecentNotifications',
|
||||||
async (_params, { getState }) => {
|
async (_params, { getState }) => {
|
||||||
return apiFetchNotificationGroups({
|
return apiFetchNotificationGroups({
|
||||||
grouped_types: supportedGroupedNotificationTypes,
|
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||||
max_id: undefined,
|
max_id: undefined,
|
||||||
exclude_types: getExcludedTypes(getState()),
|
exclude_types: getExcludedTypes(getState()),
|
||||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||||
@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||||||
|
|
||||||
dispatchAssociatedRecords(dispatch, [notification]);
|
dispatchAssociatedRecords(dispatch, [notification]);
|
||||||
|
|
||||||
return notification;
|
return {
|
||||||
|
notification,
|
||||||
|
groupedTypes: selectNotificationGroupedTypes(state),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export const allNotificationTypes = [
|
|||||||
'admin.report',
|
'admin.report',
|
||||||
'moderation_warning',
|
'moderation_warning',
|
||||||
'severed_relationships',
|
'severed_relationships',
|
||||||
|
'annual_report',
|
||||||
];
|
];
|
||||||
|
|
||||||
export type NotificationWithStatusType =
|
export type NotificationWithStatusType =
|
||||||
@ -39,7 +40,8 @@ export type NotificationType =
|
|||||||
| 'moderation_warning'
|
| 'moderation_warning'
|
||||||
| 'severed_relationships'
|
| 'severed_relationships'
|
||||||
| 'admin.sign_up'
|
| 'admin.sign_up'
|
||||||
| 'admin.report';
|
| 'admin.report'
|
||||||
|
| 'annual_report';
|
||||||
|
|
||||||
export interface BaseNotificationJSON {
|
export interface BaseNotificationJSON {
|
||||||
id: string;
|
id: string;
|
||||||
@ -132,6 +134,15 @@ interface AccountRelationshipSeveranceNotificationJSON
|
|||||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiAnnualReportEventJSON {
|
||||||
|
year: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||||
|
type: 'annual_report';
|
||||||
|
annual_report: ApiAnnualReportEventJSON;
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiNotificationJSON =
|
export type ApiNotificationJSON =
|
||||||
| SimpleNotificationJSON
|
| SimpleNotificationJSON
|
||||||
| ReportNotificationJSON
|
| ReportNotificationJSON
|
||||||
@ -144,7 +155,8 @@ export type ApiNotificationGroupJSON =
|
|||||||
| ReportNotificationGroupJSON
|
| ReportNotificationGroupJSON
|
||||||
| AccountRelationshipSeveranceNotificationGroupJSON
|
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||||
| NotificationGroupWithStatusJSON
|
| NotificationGroupWithStatusJSON
|
||||||
| ModerationWarningNotificationGroupJSON;
|
| ModerationWarningNotificationGroupJSON
|
||||||
|
| AnnualReportNotificationGroupJSON;
|
||||||
|
|
||||||
export interface ApiNotificationGroupsResultJSON {
|
export interface ApiNotificationGroupsResultJSON {
|
||||||
accounts: ApiAccountJSON[];
|
accounts: ApiAccountJSON[];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren, JSX } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
/* Significantly rewritten from upstream to keep the old design for now */
|
import { StatusBanner, BannerVariant } from './status_banner';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export const ContentWarning: React.FC<{
|
export const ContentWarning: React.FC<{
|
||||||
text: string;
|
text: string;
|
||||||
@ -8,20 +6,12 @@ export const ContentWarning: React.FC<{
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
icons?: React.ReactNode[];
|
icons?: React.ReactNode[];
|
||||||
}> = ({ text, expanded, onClick, icons }) => (
|
}> = ({ text, expanded, onClick, icons }) => (
|
||||||
<p>
|
<StatusBanner
|
||||||
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '}
|
expanded={expanded}
|
||||||
<button
|
onClick={onClick}
|
||||||
type='button'
|
variant={BannerVariant.Warning}
|
||||||
className='status__content__spoiler-link'
|
>
|
||||||
onClick={onClick}
|
{icons}
|
||||||
aria-expanded={expanded}
|
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||||
>
|
</StatusBanner>
|
||||||
{expanded ? (
|
|
||||||
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
|
||||||
)}
|
|
||||||
{icons}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
);
|
);
|
||||||
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
|||||||
<StatusBanner
|
<StatusBanner
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={BannerVariant.Blue}
|
variant={BannerVariant.Filter}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='filter_warning.matches_filter'
|
id='filter_warning.matches_filter'
|
||||||
defaultMessage='Matches filter “{title}”'
|
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||||
values={{ title }}
|
values={{
|
||||||
|
title,
|
||||||
|
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</StatusBanner>
|
</StatusBanner>
|
||||||
|
@ -98,12 +98,12 @@ class Item extends PureComponent {
|
|||||||
height = 50;
|
height = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.get('description')?.length > 0) {
|
|
||||||
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
|
if (description?.length > 0) {
|
||||||
|
badges.push(<AltTextBadge key='alt' description={description} />);
|
||||||
|
}
|
||||||
|
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||||
|
@ -13,11 +13,14 @@ class ModalRoot extends PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
backgroundColor: PropTypes.shape({
|
backgroundColor: PropTypes.oneOfType([
|
||||||
r: PropTypes.number,
|
PropTypes.string,
|
||||||
g: PropTypes.number,
|
PropTypes.shape({
|
||||||
b: PropTypes.number,
|
r: PropTypes.number,
|
||||||
}),
|
g: PropTypes.number,
|
||||||
|
b: PropTypes.number,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
noEsc: PropTypes.bool,
|
noEsc: PropTypes.bool,
|
||||||
ignoreFocus: PropTypes.bool,
|
ignoreFocus: PropTypes.bool,
|
||||||
...WithOptionalRouterPropTypes,
|
...WithOptionalRouterPropTypes,
|
||||||
@ -146,14 +149,17 @@ class ModalRoot extends PureComponent {
|
|||||||
|
|
||||||
let backgroundColor = null;
|
let backgroundColor = null;
|
||||||
|
|
||||||
if (this.props.backgroundColor) {
|
if (this.props.backgroundColor && typeof this.props.backgroundColor === 'string') {
|
||||||
backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
|
backgroundColor = this.props.backgroundColor;
|
||||||
|
} else if (this.props.backgroundColor) {
|
||||||
|
const darkenedColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
|
||||||
|
backgroundColor = `rgb(${darkenedColor.r}, ${darkenedColor.g}, ${darkenedColor.b})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root' ref={this.setRef}>
|
<div className='modal-root' ref={this.setRef}>
|
||||||
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
||||||
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
|
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor }} />
|
||||||
<div role='dialog' className='modal-root__container'>{children}</div>
|
<div role='dialog' className='modal-root__container'>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,12 +41,14 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
|||||||
class Poll extends ImmutablePureComponent {
|
class Poll extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
poll: ImmutablePropTypes.map,
|
poll: ImmutablePropTypes.map.isRequired,
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
refresh: PropTypes.func,
|
refresh: PropTypes.func,
|
||||||
onVote: PropTypes.func,
|
onVote: PropTypes.func,
|
||||||
|
onInteractionModal: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -117,7 +119,11 @@ class Poll extends ImmutablePureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onVote(Object.keys(this.state.selected));
|
if (this.props.identity.signedIn) {
|
||||||
|
this.props.onVote(Object.keys(this.state.selected));
|
||||||
|
} else {
|
||||||
|
this.props.onInteractionModal('vote', this.props.status);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRefresh = () => {
|
handleRefresh = () => {
|
||||||
@ -232,7 +238,7 @@ class Poll extends ImmutablePureComponent {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className='poll__footer'>
|
<div className='poll__footer'>
|
||||||
{!showResults && <button className='button button-secondary' disabled={disabled || !this.props.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||||
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
|
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
|
||||||
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
|
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
|
||||||
{votesCount}
|
{votesCount}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
@ -590,12 +590,15 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
let prepend, rebloggedByText;
|
let prepend, rebloggedByText;
|
||||||
|
|
||||||
|
const matchedFilters = status.get('matched_filters');
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||||
<div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
|
<div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
|
||||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||||
<span>{status.get('content')}</span>
|
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
|
||||||
|
{isExpanded && <span>{status.get('content')}</span>}
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
@ -604,7 +607,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||||
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||||
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
|
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
|
||||||
const matchedFilters = status.get('matched_filters');
|
|
||||||
|
|
||||||
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
||||||
const minHandlers = this.props.muted ? {} : {
|
const minHandlers = this.props.muted ? {} : {
|
||||||
@ -655,7 +657,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
} else if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||||
media.push(
|
media.push(
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
{Component => (
|
{Component => (
|
||||||
@ -749,7 +751,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
contentMedia.push(<PollContainer pollId={status.get('poll')} lang={language} />);
|
contentMedia.push(<PollContainer pollId={status.get('poll')} status={status} lang={language} />);
|
||||||
contentMediaIcons.push('tasks');
|
contentMediaIcons.push('tasks');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,7 +816,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||||
|
|
||||||
{(!muted || !isCollapsed) && (
|
{(!muted || !isCollapsed) && (
|
||||||
<header className='status__info'>
|
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */
|
||||||
|
<header onClick={this.parseClick} className='status__info'>
|
||||||
<StatusHeader
|
<StatusHeader
|
||||||
status={status}
|
status={status}
|
||||||
friend={account}
|
friend={account}
|
||||||
|
@ -243,7 +243,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus && (signedIn || !isRemote)) {
|
if (publicStatus && !isRemote) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export enum BannerVariant {
|
export enum BannerVariant {
|
||||||
Yellow = 'yellow',
|
Warning = 'warning',
|
||||||
Blue = 'blue',
|
Filter = 'filter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusBanner: React.FC<{
|
export const StatusBanner: React.FC<{
|
||||||
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}> = ({ children, variant, expanded, onClick }) => (
|
}> = ({ children, variant, expanded, onClick }) => (
|
||||||
<div
|
<label
|
||||||
className={
|
className={
|
||||||
variant === BannerVariant.Yellow
|
variant === BannerVariant.Warning
|
||||||
? 'content-warning'
|
? 'content-warning'
|
||||||
: 'content-warning content-warning--filter'
|
: 'content-warning content-warning--filter'
|
||||||
}
|
}
|
||||||
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
|||||||
id='content_warning.hide'
|
id='content_warning.hide'
|
||||||
defaultMessage='Hide post'
|
defaultMessage='Hide post'
|
||||||
/>
|
/>
|
||||||
|
) : variant === BannerVariant.Warning ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='content_warning.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='content_warning.show'
|
id='content_warning.show'
|
||||||
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</label>
|
||||||
);
|
);
|
||||||
|
@ -378,7 +378,7 @@ class StatusContent extends PureComponent {
|
|||||||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
||||||
|
|
||||||
let spoilerIcons = [];
|
let spoilerIcons = [];
|
||||||
if (hidden && mediaIcons) {
|
if (mediaIcons) {
|
||||||
const mediaComponents = {
|
const mediaComponents = {
|
||||||
'link': LinkIcon,
|
'link': LinkIcon,
|
||||||
'picture-o': ImageIcon,
|
'picture-o': ImageIcon,
|
||||||
|
@ -2,6 +2,7 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
|
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
|
||||||
import Poll from 'flavours/glitch/components/poll';
|
import Poll from 'flavours/glitch/components/poll';
|
||||||
|
|
||||||
@ -17,6 +18,17 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({
|
|||||||
onVote (choices) {
|
onVote (choices) {
|
||||||
dispatch(vote(pollId, choices));
|
dispatch(vote(pollId, choices));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onInteractionModal (type, status) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'INTERACTION',
|
||||||
|
modalProps: {
|
||||||
|
type,
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('uri'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { pollId }) => ({
|
const mapStateToProps = (state, { pollId }) => ({
|
||||||
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
|||||||
|
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const oldReadOnly = input.readOnly;
|
navigator.clipboard
|
||||||
|
.writeText(input.value)
|
||||||
input.readOnly = false;
|
.then(() => {
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
input.setSelectionRange(0, input.value.length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (document.execCommand('copy')) {
|
|
||||||
input.blur();
|
|
||||||
|
|
||||||
const parent = target.parentElement;
|
const parent = target.parentElement;
|
||||||
|
|
||||||
if (!parent) return;
|
if (parent) {
|
||||||
parent.classList.add('copied');
|
parent.classList.add('copied');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
parent.classList.remove('copied');
|
parent.classList.remove('copied');
|
||||||
}, 700);
|
}, 700);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.readOnly = oldReadOnly;
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import booster from '@/images/archetypes/booster.png';
|
||||||
|
import lurker from '@/images/archetypes/lurker.png';
|
||||||
|
import oracle from '@/images/archetypes/oracle.png';
|
||||||
|
import pollster from '@/images/archetypes/pollster.png';
|
||||||
|
import replier from '@/images/archetypes/replier.png';
|
||||||
|
import type { Archetype as ArchetypeData } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const Archetype: React.FC<{
|
||||||
|
data: ArchetypeData;
|
||||||
|
}> = ({ data }) => {
|
||||||
|
let illustration, label;
|
||||||
|
|
||||||
|
switch (data) {
|
||||||
|
case 'booster':
|
||||||
|
illustration = booster;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.archetype.booster'
|
||||||
|
defaultMessage='The cool-hunter'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'replier':
|
||||||
|
illustration = replier;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.archetype.replier'
|
||||||
|
defaultMessage='The social butterfly'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'pollster':
|
||||||
|
illustration = pollster;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.archetype.pollster'
|
||||||
|
defaultMessage='The pollster'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'lurker':
|
||||||
|
illustration = lurker;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.archetype.lurker'
|
||||||
|
defaultMessage='The lurker'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'oracle':
|
||||||
|
illustration = oracle;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.archetype.oracle'
|
||||||
|
defaultMessage='The oracle'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__archetype'>
|
||||||
|
<div className='annual-report__summary__archetype__label'>{label}</div>
|
||||||
|
<img src={illustration} alt='' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,69 @@
|
|||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
|
||||||
|
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||||
|
import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const Followers: React.FC<{
|
||||||
|
data: TimeSeriesMonth[];
|
||||||
|
total?: number;
|
||||||
|
}> = ({ data, total }) => {
|
||||||
|
const change = data.reduce((sum, item) => sum + item.followers, 0);
|
||||||
|
|
||||||
|
const cumulativeGraph = data.reduce(
|
||||||
|
(newData, item) => [
|
||||||
|
...newData,
|
||||||
|
item.followers + (newData[newData.length - 1] ?? 0),
|
||||||
|
],
|
||||||
|
[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__followers'>
|
||||||
|
<Sparklines data={cumulativeGraph} margin={0}>
|
||||||
|
<svg>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id='gradient' x1='0%' y1='0%' x2='0%' y2='100%'>
|
||||||
|
<stop
|
||||||
|
offset='0%'
|
||||||
|
stopColor='var(--sparkline-gradient-top)'
|
||||||
|
stopOpacity='1'
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset='100%'
|
||||||
|
stopColor='var(--sparkline-gradient-bottom)'
|
||||||
|
stopOpacity='0'
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
|
</Sparklines>
|
||||||
|
|
||||||
|
<div className='annual-report__summary__followers__foreground'>
|
||||||
|
<div className='annual-report__summary__followers__number'>
|
||||||
|
{change > -1 ? '+' : '-'}
|
||||||
|
<FormattedNumber value={change} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='annual-report__summary__followers__label'>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.followers.followers'
|
||||||
|
defaultMessage='followers'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className='annual-report__summary__followers__footnote'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.followers.total'
|
||||||
|
defaultMessage='{count} total'
|
||||||
|
values={{ count: <ShortNumber value={total ?? 0} /> }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,109 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
||||||
|
@typescript-eslint/no-explicit-any,
|
||||||
|
@typescript-eslint/no-unsafe-assignment */
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
|
||||||
|
import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
import type { TopStatuses } from 'flavours/glitch/models/annual_report';
|
||||||
|
import {
|
||||||
|
makeGetStatus,
|
||||||
|
makeGetPictureInPicture,
|
||||||
|
} from 'flavours/glitch/selectors';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||||
|
const getPictureInPicture = makeGetPictureInPicture() as unknown as (
|
||||||
|
arg0: any,
|
||||||
|
arg1: any,
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
export const HighlightedPost: React.FC<{
|
||||||
|
data: TopStatuses;
|
||||||
|
}> = ({ data }) => {
|
||||||
|
let statusId, label;
|
||||||
|
|
||||||
|
if (data.by_reblogs) {
|
||||||
|
statusId = data.by_reblogs;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.highlighted_post.by_reblogs'
|
||||||
|
defaultMessage='most boosted post'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (data.by_favourites) {
|
||||||
|
statusId = data.by_favourites;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.highlighted_post.by_favourites'
|
||||||
|
defaultMessage='most favourited post'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
statusId = data.by_replies;
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.highlighted_post.by_replies'
|
||||||
|
defaultMessage='post with the most replies'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const domain = useAppSelector((state) => state.meta.get('domain'));
|
||||||
|
const status = useAppSelector((state) =>
|
||||||
|
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||||
|
);
|
||||||
|
const pictureInPicture = useAppSelector((state) =>
|
||||||
|
statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
|
||||||
|
);
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
me ? state.accounts.get(me) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleHidden = useCallback(() => {
|
||||||
|
dispatch(toggleStatusSpoilers(statusId));
|
||||||
|
}, [dispatch, statusId]);
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-boosted-post' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = (
|
||||||
|
<span className='display-name'>
|
||||||
|
<strong className='display-name__html'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.highlighted_post.possessive'
|
||||||
|
defaultMessage="{name}'s"
|
||||||
|
values={{
|
||||||
|
name: account && (
|
||||||
|
<bdi
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='display-name__account'>{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-boosted-post'>
|
||||||
|
<DetailedStatus
|
||||||
|
status={status}
|
||||||
|
pictureInPicture={pictureInPicture}
|
||||||
|
domain={domain}
|
||||||
|
onToggleHidden={handleToggleHidden}
|
||||||
|
overrideDisplayName={displayName}
|
||||||
|
expanded={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,99 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import {
|
||||||
|
importFetchedStatuses,
|
||||||
|
importFetchedAccounts,
|
||||||
|
} from 'flavours/glitch/actions/importer';
|
||||||
|
import { apiRequestGet, apiRequestPost } from 'flavours/glitch/api';
|
||||||
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
import type { AnnualReport as AnnualReportData } from 'flavours/glitch/models/annual_report';
|
||||||
|
import type { Status } from 'flavours/glitch/models/status';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { Archetype } from './archetype';
|
||||||
|
import { Followers } from './followers';
|
||||||
|
import { HighlightedPost } from './highlighted_post';
|
||||||
|
import { MostUsedHashtag } from './most_used_hashtag';
|
||||||
|
import { NewPosts } from './new_posts';
|
||||||
|
import { Percentile } from './percentile';
|
||||||
|
|
||||||
|
interface AnnualReportResponse {
|
||||||
|
annual_reports: AnnualReportData[];
|
||||||
|
accounts: Account[];
|
||||||
|
statuses: Status[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnnualReport: React.FC<{
|
||||||
|
year: string;
|
||||||
|
}> = ({ year }) => {
|
||||||
|
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const currentAccount = useAppSelector((state) =>
|
||||||
|
me ? state.accounts.get(me) : undefined,
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
|
||||||
|
.then((data) => {
|
||||||
|
dispatch(importFetchedStatuses(data.statuses));
|
||||||
|
dispatch(importFetchedAccounts(data.accounts));
|
||||||
|
|
||||||
|
setResponse(data);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
return apiRequestPost(`v1/annual_reports/${year}/read`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [dispatch, year, setResponse, setLoading]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = response?.annual_reports[0];
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report'>
|
||||||
|
<div className='annual-report__header'>
|
||||||
|
<h1>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.thanks'
|
||||||
|
defaultMessage='Thanks for being part of Mastodon!'
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.here_it_is'
|
||||||
|
defaultMessage='Here is your {year} in review:'
|
||||||
|
values={{ year: report.year }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='annual-report__bento annual-report__summary'>
|
||||||
|
<Archetype data={report.data.archetype} />
|
||||||
|
<HighlightedPost data={report.data.top_statuses} />
|
||||||
|
<Followers
|
||||||
|
data={report.data.time_series}
|
||||||
|
total={currentAccount?.followers_count}
|
||||||
|
/>
|
||||||
|
<MostUsedHashtag data={report.data.top_hashtags} />
|
||||||
|
<Percentile data={report.data.percentiles} />
|
||||||
|
<NewPosts data={report.data.time_series} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import type { NameAndCount } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const MostUsedApp: React.FC<{
|
||||||
|
data: NameAndCount[];
|
||||||
|
}> = ({ data }) => {
|
||||||
|
const app = data[0];
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-used-app' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-used-app'>
|
||||||
|
<div className='annual-report__summary__most-used-app__icon'>
|
||||||
|
{app.name}
|
||||||
|
</div>
|
||||||
|
<div className='annual-report__summary__most-used-app__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.most_used_app.most_used_app'
|
||||||
|
defaultMessage='most used app'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import type { NameAndCount } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const MostUsedHashtag: React.FC<{
|
||||||
|
data: NameAndCount[];
|
||||||
|
}> = ({ data }) => {
|
||||||
|
const hashtag = data[0];
|
||||||
|
|
||||||
|
if (!hashtag) {
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag'>
|
||||||
|
<div className='annual-report__summary__most-used-hashtag__hashtag'>
|
||||||
|
#{hashtag.name}
|
||||||
|
</div>
|
||||||
|
<div className='annual-report__summary__most-used-hashtag__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.most_used_hashtag.most_used_hashtag'
|
||||||
|
defaultMessage='most used hashtag'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,53 @@
|
|||||||
|
import { FormattedNumber, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
|
||||||
|
import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const NewPosts: React.FC<{
|
||||||
|
data: TimeSeriesMonth[];
|
||||||
|
}> = ({ data }) => {
|
||||||
|
const posts = data.reduce((sum, item) => sum + item.statuses, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__new-posts'>
|
||||||
|
<svg width={500} height={500}>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id='posts'
|
||||||
|
x='0'
|
||||||
|
y='0'
|
||||||
|
width='32'
|
||||||
|
height='35'
|
||||||
|
patternUnits='userSpaceOnUse'
|
||||||
|
>
|
||||||
|
<circle cx='12' cy='12' r='12' fill='var(--lime)' />
|
||||||
|
<ChatBubbleIcon
|
||||||
|
fill='var(--indigo-1)'
|
||||||
|
x='4'
|
||||||
|
y='4'
|
||||||
|
width='16'
|
||||||
|
height='16'
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
fill='url(#posts)'
|
||||||
|
style={{ opacity: 0.2 }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className='annual-report__summary__new-posts__number'>
|
||||||
|
<FormattedNumber value={posts} />
|
||||||
|
</div>
|
||||||
|
<div className='annual-report__summary__new-posts__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.new_posts.new_posts'
|
||||||
|
defaultMessage='new posts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,53 @@
|
|||||||
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import type { Percentiles } from 'flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
export const Percentile: React.FC<{
|
||||||
|
data: Percentiles;
|
||||||
|
}> = ({ data }) => {
|
||||||
|
const percentile = data.statuses;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='annual-report__bento__box annual-report__summary__percentile'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.percentile.text'
|
||||||
|
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>'
|
||||||
|
values={{
|
||||||
|
topLabel: (str) => (
|
||||||
|
<div className='annual-report__summary__percentile__label'>
|
||||||
|
{str}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
percentage: () => (
|
||||||
|
<div className='annual-report__summary__percentile__number'>
|
||||||
|
<FormattedNumber
|
||||||
|
value={percentile / 100}
|
||||||
|
style='percent'
|
||||||
|
maximumFractionDigits={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
bottomLabel: (str) => (
|
||||||
|
<div>
|
||||||
|
<div className='annual-report__summary__percentile__label'>
|
||||||
|
{str}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{percentile < 6 && (
|
||||||
|
<div className='annual-report__summary__percentile__footnote'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.summary.percentile.we_wont_tell_bernie'
|
||||||
|
defaultMessage="We won't tell Bernie."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(message) => <>{message}</>}
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -27,15 +27,19 @@ class ColumnSettings extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='column-settings'>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<div className='column-settings__row'>
|
||||||
</div>
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<section>
|
||||||
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ const messages = defineMessages({
|
|||||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Single choice' },
|
||||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)}>
|
<Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='follow_requests'
|
scrollKey='follow_requests'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
@ -151,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||||
|
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||||
|
} else {
|
||||||
|
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||||
|
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||||
|
}
|
||||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
|
@ -9,6 +9,7 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import { throttle, escapeRegExp } from 'lodash';
|
import { throttle, escapeRegExp } from 'lodash';
|
||||||
|
|
||||||
|
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||||
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
@ -340,7 +341,7 @@ class InteractionModal extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
displayNameHtml: PropTypes.string,
|
displayNameHtml: PropTypes.string,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
|
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow', 'vote']),
|
||||||
onSignupClick: PropTypes.func.isRequired,
|
onSignupClick: PropTypes.func.isRequired,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
@ -377,6 +378,11 @@ class InteractionModal extends React.PureComponent {
|
|||||||
title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />;
|
title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />;
|
||||||
actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />;
|
actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />;
|
||||||
break;
|
break;
|
||||||
|
case 'vote':
|
||||||
|
icon = <Icon id='tasks' icon={InsertChartIcon} />;
|
||||||
|
title = <FormattedMessage id='interaction_modal.title.vote' defaultMessage="Vote in {name}'s poll" values={{ name }} />;
|
||||||
|
actionDescription = <FormattedMessage id='interaction_modal.description.vote' defaultMessage='With an account on Mastodon, you can vote in this poll.' />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signupButton;
|
let signupButton;
|
||||||
|
@ -40,6 +40,7 @@ class ColumnSettings extends PureComponent {
|
|||||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||||
|
const groupStr = <FormattedMessage id='notifications.column_settings.group' defaultMessage='Group' />;
|
||||||
|
|
||||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||||
@ -96,6 +97,10 @@ class ColumnSettings extends PureComponent {
|
|||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['group', 'follow']} onChange={onChange} label={groupStr} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section role='group' aria-labelledby='notifications-follow-request'>
|
<section role='group' aria-labelledby='notifications-follow-request'>
|
||||||
|
@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
}
|
}
|
||||||
} else if(path[0] === 'groupingBeta') {
|
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
|
||||||
dispatch(initializeNotifications());
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
|
|
||||||
|
if(path[0] === 'group' && path[1] === 'follow') {
|
||||||
|
dispatch(initializeNotifications());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import CelebrationIcon from '@/material-icons/400-24px/celebration.svg?react';
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import type { NotificationGroupAnnualReport } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
export const NotificationAnnualReport: React.FC<{
|
||||||
|
notification: NotificationGroupAnnualReport;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification: { annualReport }, unread }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const year = annualReport.year;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'ANNUAL_REPORT',
|
||||||
|
modalProps: { year },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [dispatch, year]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role='button'
|
||||||
|
className={classNames(
|
||||||
|
'notification-group notification-group--link notification-group--annual-report focusable',
|
||||||
|
{ 'notification-group--unread': unread },
|
||||||
|
)}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div className='notification-group__icon'>
|
||||||
|
<Icon id='celebration' icon={CelebrationIcon} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification-group__main'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.annual_report.message'
|
||||||
|
defaultMessage="Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!"
|
||||||
|
values={{ year }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<button onClick={handleClick} className='link-button'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.annual_report.view'
|
||||||
|
defaultMessage='View #Wrapstodon'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,16 +1,21 @@
|
|||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||||
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import type { NotificationGroupFollow } from 'flavours/glitch/models/notification_group';
|
import type { NotificationGroupFollow } from 'flavours/glitch/models/notification_group';
|
||||||
import { useAppSelector } from 'flavours/glitch/store';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||||
if (total === 1)
|
if (total === 1)
|
||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -23,10 +28,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
|
|||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.follow.name_and_others'
|
id='notification.follow.name_and_others'
|
||||||
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
|
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you'
|
||||||
values={{
|
values={{
|
||||||
name: displayedName,
|
name: displayedName,
|
||||||
count: total - 1,
|
count: total - 1,
|
||||||
|
a: (chunks) =>
|
||||||
|
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -46,6 +53,10 @@ export const NotificationFollow: React.FC<{
|
|||||||
notification: NotificationGroupFollow;
|
notification: NotificationGroupFollow;
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
}> = ({ notification, unread }) => {
|
}> = ({ notification, unread }) => {
|
||||||
|
const username = useAppSelector(
|
||||||
|
(state) => state.accounts.getIn([me, 'username']) as string,
|
||||||
|
);
|
||||||
|
|
||||||
let actions: JSX.Element | undefined;
|
let actions: JSX.Element | undefined;
|
||||||
let additionalContent: JSX.Element | undefined;
|
let additionalContent: JSX.Element | undefined;
|
||||||
|
|
||||||
@ -68,6 +79,7 @@ export const NotificationFollow: React.FC<{
|
|||||||
timestamp={notification.latest_page_notification_at}
|
timestamp={notification.latest_page_notification_at}
|
||||||
count={notification.notifications_count}
|
count={notification.notifications_count}
|
||||||
labelRenderer={labelRenderer}
|
labelRenderer={labelRenderer}
|
||||||
|
labelSeeMoreHref={`/@${username}/followers`}
|
||||||
unread={unread}
|
unread={unread}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
additionalContent={additionalContent}
|
additionalContent={additionalContent}
|
||||||
|
@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
|||||||
|
|
||||||
import { NotificationAdminReport } from './notification_admin_report';
|
import { NotificationAdminReport } from './notification_admin_report';
|
||||||
import { NotificationAdminSignUp } from './notification_admin_sign_up';
|
import { NotificationAdminSignUp } from './notification_admin_sign_up';
|
||||||
|
import { NotificationAnnualReport } from './notification_annual_report';
|
||||||
import { NotificationFavourite } from './notification_favourite';
|
import { NotificationFavourite } from './notification_favourite';
|
||||||
import { NotificationFollow } from './notification_follow';
|
import { NotificationFollow } from './notification_follow';
|
||||||
import { NotificationFollowRequest } from './notification_follow_request';
|
import { NotificationFollowRequest } from './notification_follow_request';
|
||||||
@ -152,6 +153,14 @@ export const NotificationGroup: React.FC<{
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'annual_report':
|
||||||
|
content = (
|
||||||
|
<NotificationAnnualReport
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|||||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||||
|
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
|
||||||
|
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
|
||||||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
|
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
@ -161,16 +163,20 @@ class Footer extends ImmutablePureComponent {
|
|||||||
replyTitle = intl.formatMessage(messages.replyAll);
|
replyTitle = intl.formatMessage(messages.replyAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reblogTitle = '';
|
let reblogTitle, reblogIconComponent;
|
||||||
|
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||||
|
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
|
||||||
} else if (publicStatus) {
|
} else if (publicStatus) {
|
||||||
reblogTitle = intl.formatMessage(messages.reblog);
|
reblogTitle = intl.formatMessage(messages.reblog);
|
||||||
|
reblogIconComponent = RepeatIcon;
|
||||||
} else if (reblogPrivate) {
|
} else if (reblogPrivate) {
|
||||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||||
|
reblogIconComponent = RepeatPrivateIcon;
|
||||||
} else {
|
} else {
|
||||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||||
|
reblogIconComponent = RepeatDisabledIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
let replyButton = null;
|
let replyButton = null;
|
||||||
@ -201,7 +207,7 @@ class Footer extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='picture-in-picture__footer'>
|
<div className='picture-in-picture__footer'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={status.get('url')} />}
|
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={status.get('url')} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,6 +54,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
domain: string;
|
domain: string;
|
||||||
showMedia?: boolean;
|
showMedia?: boolean;
|
||||||
withLogo?: boolean;
|
withLogo?: boolean;
|
||||||
|
overrideDisplayName?: React.ReactNode;
|
||||||
pictureInPicture: any;
|
pictureInPicture: any;
|
||||||
onToggleHidden?: (status: any) => void;
|
onToggleHidden?: (status: any) => void;
|
||||||
onToggleMediaVisibility?: () => void;
|
onToggleMediaVisibility?: () => void;
|
||||||
@ -70,6 +71,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
domain,
|
domain,
|
||||||
showMedia,
|
showMedia,
|
||||||
withLogo,
|
withLogo,
|
||||||
|
overrideDisplayName,
|
||||||
pictureInPicture,
|
pictureInPicture,
|
||||||
onToggleMediaVisibility,
|
onToggleMediaVisibility,
|
||||||
onToggleHidden,
|
onToggleHidden,
|
||||||
@ -204,7 +206,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
) {
|
) {
|
||||||
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
||||||
} else if (
|
} else if (
|
||||||
['image', 'gifv'].includes(
|
['image', 'gifv', 'unknown'].includes(
|
||||||
status.getIn(['media_attachments', 0, 'type']) as string,
|
status.getIn(['media_attachments', 0, 'type']) as string,
|
||||||
) ||
|
) ||
|
||||||
status.get('media_attachments').size > 1
|
status.get('media_attachments').size > 1
|
||||||
@ -297,6 +299,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
<PollContainer
|
<PollContainer
|
||||||
pollId={status.get('poll')}
|
pollId={status.get('poll')}
|
||||||
// @ts-expect-error -- Poll/PollContainer is not typed yet
|
// @ts-expect-error -- Poll/PollContainer is not typed yet
|
||||||
|
status={status}
|
||||||
lang={status.get('language')}
|
lang={status.get('language')}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@ -385,7 +388,11 @@ export const DetailedStatus: React.FC<{
|
|||||||
<div className='detailed-status__display-avatar'>
|
<div className='detailed-status__display-avatar'>
|
||||||
<Avatar account={status.get('account')} size={46} />
|
<Avatar account={status.get('account')} size={46} />
|
||||||
</div>
|
</div>
|
||||||
<DisplayName account={status.get('account')} localDomain={domain} />
|
|
||||||
|
{overrideDisplayName ?? (
|
||||||
|
<DisplayName account={status.get('account')} localDomain={domain} />
|
||||||
|
)}
|
||||||
|
|
||||||
{withLogo && (
|
{withLogo && (
|
||||||
<>
|
<>
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { AnnualReport } from 'flavours/glitch/features/annual_report';
|
||||||
|
|
||||||
|
const AnnualReportModal: React.FC<{
|
||||||
|
year: string;
|
||||||
|
onChangeBackgroundColor: (arg0: string) => void;
|
||||||
|
}> = ({ year, onChangeBackgroundColor }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeBackgroundColor('var(--indigo-1)');
|
||||||
|
}, [onChangeBackgroundColor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal annual-report-modal'>
|
||||||
|
<AnnualReport year={year} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default AnnualReportModal;
|
@ -20,6 +20,7 @@ import {
|
|||||||
SubscribedLanguagesModal,
|
SubscribedLanguagesModal,
|
||||||
ClosedRegistrationsModal,
|
ClosedRegistrationsModal,
|
||||||
IgnoreNotificationsModal,
|
IgnoreNotificationsModal,
|
||||||
|
AnnualReportModal,
|
||||||
} from 'flavours/glitch/features/ui/util/async-components';
|
} from 'flavours/glitch/features/ui/util/async-components';
|
||||||
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
|
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
|
||||||
|
|
||||||
@ -82,6 +83,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'INTERACTION': InteractionModal,
|
'INTERACTION': InteractionModal,
|
||||||
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
||||||
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||||
|
'ANNUAL_REPORT': AnnualReportModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends PureComponent {
|
export default class ModalRoot extends PureComponent {
|
||||||
|
@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
|
|||||||
<div className='safety-action-modal__bottom__collapsible'>
|
<div className='safety-action-modal__bottom__collapsible'>
|
||||||
<div className='safety-action-modal__field-group'>
|
<div className='safety-action-modal__field-group'>
|
||||||
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||||
|
<RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||||
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||||
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||||
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||||
|
@ -229,3 +229,7 @@ export function NotificationRequest () {
|
|||||||
export function LinkTimeline () {
|
export function LinkTimeline () {
|
||||||
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
|
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AnnualReportModal () {
|
||||||
|
return import(/*webpackChunkName: "flavours/glitch/async/modals/annual_report_modal" */'../components/annual_report_modal');
|
||||||
|
}
|
||||||
|
@ -154,7 +154,5 @@
|
|||||||
"status.is_poll": "Dieser Toot ist eine Umfrage",
|
"status.is_poll": "Dieser Toot ist eine Umfrage",
|
||||||
"status.local_only": "Nur auf deiner Instanz sichtbar",
|
"status.local_only": "Nur auf deiner Instanz sichtbar",
|
||||||
"status.show_filter_reason": "Trotzdem anzeigen",
|
"status.show_filter_reason": "Trotzdem anzeigen",
|
||||||
"status.show_less": "Weniger anzeigen",
|
|
||||||
"status.show_more": "Mehr anzeigen",
|
|
||||||
"status.uncollapse": "Ausklappen"
|
"status.uncollapse": "Ausklappen"
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,5 @@
|
|||||||
"status.local_only": "Only visible from your instance",
|
"status.local_only": "Only visible from your instance",
|
||||||
"status.react": "React",
|
"status.react": "React",
|
||||||
"status.show_filter_reason": "Show anyway",
|
"status.show_filter_reason": "Show anyway",
|
||||||
"status.show_less": "Show less",
|
|
||||||
"status.show_more": "Show more",
|
|
||||||
"status.uncollapse": "Uncollapse"
|
"status.uncollapse": "Uncollapse"
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,5 @@
|
|||||||
"status.is_poll": "Esta publicación es una encuesta",
|
"status.is_poll": "Esta publicación es una encuesta",
|
||||||
"status.local_only": "Sólo visible para tu instancia",
|
"status.local_only": "Sólo visible para tu instancia",
|
||||||
"status.show_filter_reason": "Mostrar de todos modos",
|
"status.show_filter_reason": "Mostrar de todos modos",
|
||||||
"status.show_less": "Mostrar menos",
|
|
||||||
"status.show_more": "Mostrar más",
|
|
||||||
"status.uncollapse": "Descolapsar"
|
"status.uncollapse": "Descolapsar"
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,5 @@
|
|||||||
"status.is_poll": "이 글은 설문입니다",
|
"status.is_poll": "이 글은 설문입니다",
|
||||||
"status.local_only": "당신의 서버에서만 보입니다",
|
"status.local_only": "당신의 서버에서만 보입니다",
|
||||||
"status.show_filter_reason": "그냥 표시하기",
|
"status.show_filter_reason": "그냥 표시하기",
|
||||||
"status.show_less": "접기",
|
|
||||||
"status.show_more": "더보기",
|
|
||||||
"status.uncollapse": "펼치기"
|
"status.uncollapse": "펼치기"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,88 @@
|
|||||||
{
|
{
|
||||||
"about.fork_disclaimer": "Glitch-soc - это бесплатное программное обеспечение с открытым исходным кодом, обращенное от Mastodon.",
|
"about.fork_disclaimer": "Glitch-soc — это свободное программное обеспечение с открытым исходным кодом, ответвлённое от Mastodon.",
|
||||||
|
"account.follows": "Подписки",
|
||||||
|
"account.follows_you": "Подписан(а) на вас",
|
||||||
|
"account.suspended_disclaimer_full": "Этот пользователь был заблокирован модератором.",
|
||||||
|
"boost_modal.missing_description": "Этот пост содержит медиафайлы без описания",
|
||||||
|
"column.favourited_by": "Добавили в избранное",
|
||||||
|
"column.reblogged_by": "Продвинули",
|
||||||
|
"column_header.profile": "Профиль",
|
||||||
|
"column_subheading.lists": "Списки",
|
||||||
|
"column_subheading.navigation": "Навигация",
|
||||||
|
"compose.attach.doodle": "Нарисовать что-нибудь",
|
||||||
|
"compose.change_federation": "Изменить настройки федерации",
|
||||||
|
"compose.content-type.html": "HTML",
|
||||||
|
"compose.content-type.markdown": "Markdown",
|
||||||
|
"compose.content-type.plain": "Простой текст",
|
||||||
|
"compose.disable_threaded_mode": "Отключить режим треда",
|
||||||
|
"compose.enable_threaded_mode": "Включить режим треда",
|
||||||
|
"confirmation_modal.do_not_ask_again": "Больше не спрашивать подтверждение",
|
||||||
|
"confirmations.deprecated_settings.confirm": "Использовать настройки Mastodon",
|
||||||
|
"confirmations.missing_media_description.confirm": "Всё равно опубликовать",
|
||||||
|
"direct.group_by_conversations": "Группировать по перепискам",
|
||||||
|
"endorsed_accounts_editor.endorsed_accounts": "Рекомендованные аккаунты",
|
||||||
|
"favourite_modal.favourite": "Добавить пост в избранное?",
|
||||||
|
"federation.federated.long": "Разрешить делиться этим постом с другими серверами",
|
||||||
|
"federation.local_only.long": "Запретить делиться этим постом с другими серверами",
|
||||||
|
"home.column_settings.advanced": "Продвинутые настройки",
|
||||||
|
"home.column_settings.filter_regex": "Фильтр по регулярным выражениям",
|
||||||
|
"keyboard_shortcuts.bookmark": "добавить закладку",
|
||||||
|
"keyboard_shortcuts.toggle_collapse": "свернуть/развернуть пост",
|
||||||
|
"moved_to_warning": "Этот аккаунт переехал на {moved_to_link}, и скорее всего не принимает новых подписчиков.",
|
||||||
|
"navigation_bar.app_settings": "Настройки приложения",
|
||||||
|
"navigation_bar.keyboard_shortcuts": "Сочетания клавиш",
|
||||||
|
"notification.markForDeletion": "Отметить для удаления",
|
||||||
|
"notification_purge.btn_all": "Выбрать все",
|
||||||
|
"notification_purge.btn_apply": "Удалить выбранное",
|
||||||
|
"notification_purge.btn_invert": "Инвертировать выбор",
|
||||||
|
"notification_purge.btn_none": "Отменить выбор",
|
||||||
|
"notification_purge.start": "Войти в режим очистки уведомлений",
|
||||||
|
"notifications.column_settings.filter_bar.show_bar": "Показать панель фильтров",
|
||||||
|
"notifications.marked_clear": "Удалить выбранные уведомления",
|
||||||
|
"notifications.marked_clear_confirmation": "Вы уверены, что хотите безвозвратно удалить все выбранные уведомления?",
|
||||||
|
"settings.auto_collapse": "Сворачивать автоматически",
|
||||||
|
"settings.auto_collapse_all": "Всё",
|
||||||
|
"settings.auto_collapse_height": "Высота (в пикселях) для того, чтобы пост считался длинным",
|
||||||
|
"settings.auto_collapse_lengthy": "Длинные посты",
|
||||||
|
"settings.auto_collapse_media": "Посты с медиафайлами",
|
||||||
|
"settings.auto_collapse_notifications": "Уведомления",
|
||||||
|
"settings.auto_collapse_reblogs": "Продвижения",
|
||||||
|
"settings.auto_collapse_replies": "Ответы",
|
||||||
|
"settings.close": "Закрыть",
|
||||||
|
"settings.collapsed_statuses": "Сворачивание постов",
|
||||||
|
"settings.compose_box_opts": "Форма постинга",
|
||||||
"settings.content_warnings": "Content warnings",
|
"settings.content_warnings": "Content warnings",
|
||||||
"settings.preferences": "Preferences"
|
"settings.content_warnings.regexp": "Регулярное выражение",
|
||||||
|
"settings.content_warnings_unfold_opts": "Автоматическое раскрытие",
|
||||||
|
"settings.deprecated_setting": "Эта опция теперь может быть включена в {settings_page_link} Mastodon",
|
||||||
|
"settings.enable_collapsed": "Включить сворачивание постов",
|
||||||
|
"settings.general": "Общие",
|
||||||
|
"settings.hicolor_privacy_icons": "Цветные значки публичности поста",
|
||||||
|
"settings.hicolor_privacy_icons.hint": "Отображать значки публичности поста в ярких и различимых цветах",
|
||||||
|
"settings.media": "Медиафайлы",
|
||||||
|
"settings.notifications.favicon_badge": "Индикатор уведомлений на иконке сайта",
|
||||||
|
"settings.notifications_opts": "Опции уведомлений",
|
||||||
|
"settings.pop_in_left": "Слева",
|
||||||
|
"settings.pop_in_player": "Включить плавающий плеер",
|
||||||
|
"settings.pop_in_position": "Расположение плавающего плеера:",
|
||||||
|
"settings.pop_in_right": "Справа",
|
||||||
|
"settings.preferences": "Preferences",
|
||||||
|
"settings.shared_settings_link": "настройках пользователя",
|
||||||
|
"settings.show_reply_counter": "Показывать приблизительное число ответов",
|
||||||
|
"settings.side_arm": "Дополнительная кнопка постинга:",
|
||||||
|
"settings.side_arm.none": "Нет",
|
||||||
|
"settings.side_arm_reply_mode": "При ответе на пост дополнительная кнопка постинга должна:",
|
||||||
|
"settings.status_icons": "Значки постов",
|
||||||
|
"settings.status_icons_language": "Индикатор языка",
|
||||||
|
"settings.status_icons_local_only": "Индикатор нефедерируемого поста",
|
||||||
|
"settings.status_icons_media": "Индикаторы медиафайлов и опросов",
|
||||||
|
"settings.status_icons_reply": "Индикатор ответа",
|
||||||
|
"settings.status_icons_visibility": "Индикатор публичности поста",
|
||||||
|
"settings.tag_misleading_links": "Помечать обманчивые ссылки",
|
||||||
|
"status.collapse": "Свернуть",
|
||||||
|
"status.hide": "Скрыть пост",
|
||||||
|
"status.in_reply_to": "Этот пост является ответом",
|
||||||
|
"status.is_poll": "Этот пост содержит опрос",
|
||||||
|
"status.show_filter_reason": "Всё равно показать",
|
||||||
|
"status.uncollapse": "Развернуть"
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,5 @@
|
|||||||
"status.is_poll": "此嘟文是投票",
|
"status.is_poll": "此嘟文是投票",
|
||||||
"status.local_only": "此嘟文仅本站可见",
|
"status.local_only": "此嘟文仅本站可见",
|
||||||
"status.show_filter_reason": "仍然显示",
|
"status.show_filter_reason": "仍然显示",
|
||||||
"status.show_less": "部分显示",
|
|
||||||
"status.show_more": "完全显示",
|
|
||||||
"status.uncollapse": "展开"
|
"status.uncollapse": "展开"
|
||||||
}
|
}
|
||||||
|
44
app/javascript/flavours/glitch/models/annual_report.ts
Normal file
44
app/javascript/flavours/glitch/models/annual_report.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export interface Percentiles {
|
||||||
|
followers: number;
|
||||||
|
statuses: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NameAndCount {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesMonth {
|
||||||
|
month: number;
|
||||||
|
statuses: number;
|
||||||
|
following: number;
|
||||||
|
followers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TopStatuses {
|
||||||
|
by_reblogs: number;
|
||||||
|
by_favourites: number;
|
||||||
|
by_replies: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Archetype =
|
||||||
|
| 'lurker'
|
||||||
|
| 'booster'
|
||||||
|
| 'pollster'
|
||||||
|
| 'replier'
|
||||||
|
| 'oracle';
|
||||||
|
|
||||||
|
interface AnnualReportV1 {
|
||||||
|
most_used_apps: NameAndCount[];
|
||||||
|
percentiles: Percentiles;
|
||||||
|
top_hashtags: NameAndCount[];
|
||||||
|
top_statuses: TopStatuses;
|
||||||
|
time_series: TimeSeriesMonth[];
|
||||||
|
archetype: Archetype;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnualReport {
|
||||||
|
year: number;
|
||||||
|
schema_version: number;
|
||||||
|
data: AnnualReportV1;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiAccountRelationshipSeveranceEventJSON,
|
ApiAccountRelationshipSeveranceEventJSON,
|
||||||
ApiAccountWarningJSON,
|
ApiAccountWarningJSON,
|
||||||
|
ApiAnnualReportEventJSON,
|
||||||
BaseNotificationGroupJSON,
|
BaseNotificationGroupJSON,
|
||||||
ApiNotificationGroupJSON,
|
ApiNotificationGroupJSON,
|
||||||
ApiNotificationJSON,
|
ApiNotificationJSON,
|
||||||
@ -66,6 +67,12 @@ export interface NotificationGroupSeveredRelationships
|
|||||||
event: AccountRelationshipSeveranceEvent;
|
event: AccountRelationshipSeveranceEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnnualReportEvent = ApiAnnualReportEventJSON;
|
||||||
|
export interface NotificationGroupAnnualReport
|
||||||
|
extends BaseNotification<'annual_report'> {
|
||||||
|
annualReport: AnnualReportEvent;
|
||||||
|
}
|
||||||
|
|
||||||
interface Report extends Omit<ApiReportJSON, 'target_account'> {
|
interface Report extends Omit<ApiReportJSON, 'target_account'> {
|
||||||
targetAccountId: string;
|
targetAccountId: string;
|
||||||
}
|
}
|
||||||
@ -88,7 +95,8 @@ export type NotificationGroup =
|
|||||||
| NotificationGroupModerationWarning
|
| NotificationGroupModerationWarning
|
||||||
| NotificationGroupSeveredRelationships
|
| NotificationGroupSeveredRelationships
|
||||||
| NotificationGroupAdminSignUp
|
| NotificationGroupAdminSignUp
|
||||||
| NotificationGroupAdminReport;
|
| NotificationGroupAdminReport
|
||||||
|
| NotificationGroupAnnualReport;
|
||||||
|
|
||||||
function createReportFromJSON(reportJSON: ApiReportJSON): Report {
|
function createReportFromJSON(reportJSON: ApiReportJSON): Report {
|
||||||
const { target_account, ...report } = reportJSON;
|
const { target_account, ...report } = reportJSON;
|
||||||
@ -114,6 +122,12 @@ function createAccountRelationshipSeveranceEventFromJSON(
|
|||||||
return eventJson;
|
return eventJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAnnualReportEventFromJSON(
|
||||||
|
eventJson: ApiAnnualReportEventJSON,
|
||||||
|
): AnnualReportEvent {
|
||||||
|
return eventJson;
|
||||||
|
}
|
||||||
|
|
||||||
export function createNotificationGroupFromJSON(
|
export function createNotificationGroupFromJSON(
|
||||||
groupJson: ApiNotificationGroupJSON,
|
groupJson: ApiNotificationGroupJSON,
|
||||||
): NotificationGroup {
|
): NotificationGroup {
|
||||||
@ -148,7 +162,6 @@ export function createNotificationGroupFromJSON(
|
|||||||
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
|
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'moderation_warning': {
|
case 'moderation_warning': {
|
||||||
const { moderation_warning, ...groupWithoutModerationWarning } = group;
|
const { moderation_warning, ...groupWithoutModerationWarning } = group;
|
||||||
return {
|
return {
|
||||||
@ -157,6 +170,14 @@ export function createNotificationGroupFromJSON(
|
|||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'annual_report': {
|
||||||
|
const { annual_report, ...groupWithoutAnnualReport } = group;
|
||||||
|
return {
|
||||||
|
...groupWithoutAnnualReport,
|
||||||
|
annualReport: createAnnualReportEventFromJSON(annual_report),
|
||||||
|
sampleAccountIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
|||||||
return state.setIn([action.payload.id, 'hidden'], false);
|
return state.setIn([action.payload.id, 'hidden'], false);
|
||||||
else if (importAccounts.match(action))
|
else if (importAccounts.match(action))
|
||||||
return normalizeAccounts(state, action.payload.accounts);
|
return normalizeAccounts(state, action.payload.accounts);
|
||||||
else if (followAccountSuccess.match(action)) {
|
else if (
|
||||||
|
followAccountSuccess.match(action) &&
|
||||||
|
!action.payload.alreadyFollowing
|
||||||
|
) {
|
||||||
return state
|
return state
|
||||||
.update(action.payload.relationship.id, (account) =>
|
.update(action.payload.relationship.id, (account) =>
|
||||||
account?.update('followers_count', (n) => n + 1),
|
account?.update('followers_count', (n) => n + 1),
|
||||||
|
@ -21,7 +21,6 @@ import {
|
|||||||
unmountNotifications,
|
unmountNotifications,
|
||||||
refreshStaleNotificationGroups,
|
refreshStaleNotificationGroups,
|
||||||
pollRecentNotifications,
|
pollRecentNotifications,
|
||||||
shouldGroupNotificationType,
|
|
||||||
} from 'flavours/glitch/actions/notification_groups';
|
} from 'flavours/glitch/actions/notification_groups';
|
||||||
import {
|
import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
@ -30,6 +29,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
ApiNotificationJSON,
|
ApiNotificationJSON,
|
||||||
ApiNotificationGroupJSON,
|
ApiNotificationGroupJSON,
|
||||||
|
NotificationType,
|
||||||
} from 'flavours/glitch/api_types/notifications';
|
} from 'flavours/glitch/api_types/notifications';
|
||||||
import { compareId } from 'flavours/glitch/compare_id';
|
import { compareId } from 'flavours/glitch/compare_id';
|
||||||
import { usePendingItems } from 'flavours/glitch/initial_state';
|
import { usePendingItems } from 'flavours/glitch/initial_state';
|
||||||
@ -205,8 +205,9 @@ function mergeGapsAround(
|
|||||||
function processNewNotification(
|
function processNewNotification(
|
||||||
groups: NotificationGroupsState['groups'],
|
groups: NotificationGroupsState['groups'],
|
||||||
notification: ApiNotificationJSON,
|
notification: ApiNotificationJSON,
|
||||||
|
groupedTypes: NotificationType[],
|
||||||
) {
|
) {
|
||||||
if (!shouldGroupNotificationType(notification.type)) {
|
if (!groupedTypes.includes(notification.type)) {
|
||||||
notification = {
|
notification = {
|
||||||
...notification,
|
...notification,
|
||||||
group_key: `ungrouped-${notification.id}`,
|
group_key: `ungrouped-${notification.id}`,
|
||||||
@ -476,11 +477,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||||||
trimNotifications(state);
|
trimNotifications(state);
|
||||||
})
|
})
|
||||||
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
||||||
const notification = action.payload;
|
if (action.payload) {
|
||||||
if (notification) {
|
const { notification, groupedTypes } = action.payload;
|
||||||
|
|
||||||
processNewNotification(
|
processNewNotification(
|
||||||
usePendingItems ? state.pendingGroups : state.groups,
|
usePendingItems ? state.pendingGroups : state.groups,
|
||||||
notification,
|
notification,
|
||||||
|
groupedTypes,
|
||||||
);
|
);
|
||||||
updateLastReadId(state);
|
updateLastReadId(state);
|
||||||
trimNotifications(state);
|
trimNotifications(state);
|
||||||
|
@ -84,6 +84,10 @@ const initialState = ImmutableMap({
|
|||||||
'admin.sign_up': true,
|
'admin.sign_up': true,
|
||||||
'admin.report': true,
|
'admin.report': true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
group: ImmutableMap({
|
||||||
|
follow: true
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
firehose: ImmutableMap({
|
firehose: ImmutableMap({
|
||||||
|
@ -52,4 +52,7 @@ export const selectSettingsNotificationsMinimizeFilteredBanner = (
|
|||||||
) =>
|
) =>
|
||||||
state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean;
|
state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean;
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsGroupFollows = (state: RootState) =>
|
||||||
|
state.settings.getIn(['notifications', 'group', 'follow']) as boolean;
|
||||||
|
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||||
|
335
app/javascript/flavours/glitch/styles/annual_reports.scss
Normal file
335
app/javascript/flavours/glitch/styles/annual_reports.scss
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
:root {
|
||||||
|
--indigo-1: #17063b;
|
||||||
|
--indigo-2: #2f0c7a;
|
||||||
|
--indigo-3: #562cfc;
|
||||||
|
--indigo-5: #858afa;
|
||||||
|
--indigo-6: #cccfff;
|
||||||
|
--lime: #baff3b;
|
||||||
|
--goldenrod-2: #ffc954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annual-report {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: var(--indigo-1);
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 30px;
|
||||||
|
color: var(--lime);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--indigo-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bento {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax(
|
||||||
|
0,
|
||||||
|
auto
|
||||||
|
);
|
||||||
|
|
||||||
|
&__box {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--indigo-2);
|
||||||
|
color: var(--indigo-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
&__most-boosted-post {
|
||||||
|
grid-column: span 2;
|
||||||
|
grid-row: span 2;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.status__content,
|
||||||
|
.content-warning {
|
||||||
|
color: var(--indigo-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-warning {
|
||||||
|
border: 0;
|
||||||
|
background: var(--indigo-1);
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
color: var(--indigo-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status__meta__line {
|
||||||
|
border-bottom-color: var(--indigo-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status__meta {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status__meta,
|
||||||
|
.poll__footer,
|
||||||
|
.poll__link,
|
||||||
|
.detailed-status .logo,
|
||||||
|
.detailed-status__display-name {
|
||||||
|
color: var(--indigo-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status__meta .animated-number,
|
||||||
|
.detailed-status__display-name strong {
|
||||||
|
color: var(--indigo-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll__chart {
|
||||||
|
background-color: var(--indigo-3);
|
||||||
|
|
||||||
|
&.leading {
|
||||||
|
background-color: var(--goldenrod-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__followers {
|
||||||
|
grid-column: span 1;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-block-start: 24px;
|
||||||
|
padding-block-end: 24px;
|
||||||
|
|
||||||
|
--sparkline-gradient-top: rgba(86, 44, 252, 50%);
|
||||||
|
--sparkline-gradient-bottom: rgba(86, 44, 252, 0%);
|
||||||
|
|
||||||
|
&__foreground {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__number {
|
||||||
|
font-size: 31px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 37px;
|
||||||
|
color: var(--lime);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 17px;
|
||||||
|
color: var(--indigo-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footnote {
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
height: 70%;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
path:first-child {
|
||||||
|
fill: url('#gradient') !important;
|
||||||
|
fill-opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
path:last-child {
|
||||||
|
stroke: var(--indigo-3) !important;
|
||||||
|
fill: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__archetype {
|
||||||
|
grid-column: span 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--lime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__most-used-app {
|
||||||
|
grid-column: span 1;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--indigo-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--goldenrod-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__percentile {
|
||||||
|
grid-row: span 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
padding: 16px 8px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__number {
|
||||||
|
font-size: 61px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 73px;
|
||||||
|
color: var(--goldenrod-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footnote {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-posts {
|
||||||
|
grid-column: span 2;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--indigo-6);
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__number {
|
||||||
|
font-size: 76px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 91px;
|
||||||
|
color: var(--goldenrod-2);
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: -7px;
|
||||||
|
top: -4px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__most-used-hashtag {
|
||||||
|
grid-column: span 2;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__hashtag {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 58px;
|
||||||
|
color: var(--indigo-6);
|
||||||
|
margin-inline-start: -100%;
|
||||||
|
margin-inline-end: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.annual-report-modal {
|
||||||
|
max-width: 480px;
|
||||||
|
background: var(--indigo-1);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.loading-indicator .circular-progress {
|
||||||
|
color: var(--lime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-columns-breakpoint) {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-group--annual-report {
|
||||||
|
.notification-group__icon {
|
||||||
|
color: var(--lime);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-group__main .link-button {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--lime);
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
@import 'polls';
|
@import 'polls';
|
||||||
@import 'modal';
|
@import 'modal';
|
||||||
@import 'emoji_picker';
|
@import 'emoji_picker';
|
||||||
|
@import 'annual_reports';
|
||||||
@import 'about';
|
@import 'about';
|
||||||
@import 'tables';
|
@import 'tables';
|
||||||
@import 'admin';
|
@import 'admin';
|
||||||
|
@ -1399,9 +1399,9 @@ body > [data-popper-placement] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status__content__spoiler-link {
|
.status__content__spoiler-link {
|
||||||
display: inline-flex; // glitch: media icon in spoiler button
|
display: inline-block;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: $action-button-color; // glitch: design used in more places
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -1411,23 +1411,6 @@ body > [data-popper-placement] {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
align-items: center; // glitch: content indicator
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
// glitch: design used in more places
|
|
||||||
background: lighten($action-button-color, 7%);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content__spoiler-icon {
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-start: 5px;
|
|
||||||
border-inline-start: 1px solid currentColor;
|
|
||||||
padding: 0;
|
|
||||||
padding-inline-start: 4px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__wrapper--filtered {
|
.status__wrapper--filtered {
|
||||||
@ -1878,7 +1861,8 @@ body > [data-popper-placement] {
|
|||||||
|
|
||||||
.status__wrapper-direct,
|
.status__wrapper-direct,
|
||||||
.notification-ungrouped--direct,
|
.notification-ungrouped--direct,
|
||||||
.notification-group--direct {
|
.notification-group--direct,
|
||||||
|
.notification-group--annual-report {
|
||||||
background: rgba($ui-highlight-color, 0.05);
|
background: rgba($ui-highlight-color, 0.05);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -1952,6 +1936,14 @@ body > [data-popper-placement] {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-warning {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -2954,6 +2946,7 @@ a.account__display-name {
|
|||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
contain: inline-size layout paint style;
|
||||||
|
|
||||||
@media screen and (min-width: $no-gap-breakpoint) {
|
@media screen and (min-width: $no-gap-breakpoint) {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@ -4250,6 +4243,7 @@ input.glitch-setting-text {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--background-border-color);
|
border: 1px solid var(--background-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
contain: inline-size layout paint style;
|
||||||
|
|
||||||
&.bottomless {
|
&.bottomless {
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
@ -6265,7 +6259,8 @@ a.status-card {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba($base-overlay-background, 0.7);
|
opacity: 0.9;
|
||||||
|
background: $base-overlay-background;
|
||||||
transition: background 0.5s;
|
transition: background 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6292,6 +6287,7 @@ a.status-card {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
@media screen and (width <= $mobile-breakpoint) {
|
@media screen and (width <= $mobile-breakpoint) {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
@ -11377,21 +11373,17 @@ noscript {
|
|||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
-webkit-line-clamp: 4;
|
-webkit-line-clamp: 4;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
max-height: 4 * 22px;
|
max-height: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
p {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
p,
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-indicator__attachments {
|
.reply-indicator__attachments {
|
||||||
@ -11676,19 +11668,21 @@ noscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-warning {
|
.content-warning {
|
||||||
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba($ui-highlight-color, 0.05);
|
background: rgba($ui-highlight-color, 0.05);
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
border-top: 1px solid;
|
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||||
border-bottom: 1px solid;
|
border-radius: 8px;
|
||||||
border-color: rgba($ui-highlight-color, 0.15);
|
|
||||||
padding: 8px (5px + 8px);
|
padding: 8px (5px + 8px);
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-button {
|
.link-button {
|
||||||
@ -11697,31 +11691,22 @@ noscript {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&--filter {
|
||||||
&::after {
|
color: $darker-text-color;
|
||||||
content: '';
|
|
||||||
display: block;
|
p {
|
||||||
position: absolute;
|
font-weight: normal;
|
||||||
height: 100%;
|
}
|
||||||
background: url('~images/warning-stripes.svg') repeat-y;
|
|
||||||
width: 5px;
|
.filter-name {
|
||||||
top: 0;
|
font-weight: 500;
|
||||||
|
color: $secondary-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
.status__content__spoiler-icon {
|
||||||
border-start-start-radius: 4px;
|
float: inline-end;
|
||||||
border-end-start-radius: 4px;
|
width: 20px;
|
||||||
inset-inline-start: 0;
|
height: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-start-end-radius: 4px;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
inset-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--filter::before,
|
|
||||||
&--filter::after {
|
|
||||||
background-image: url('~images/filter-stripes.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ code {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
|
max-width: 566px;
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -76,4 +76,7 @@ body {
|
|||||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||||
--background-filter: blur(10px);
|
--background-filter: blur(10px);
|
||||||
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
||||||
|
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||||
|
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,29 @@
|
|||||||
.e-content,
|
.e-content,
|
||||||
.edit-indicator__content,
|
.edit-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
|
code {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
@ -14,19 +34,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-inline-start: 10px;
|
padding-inline-start: 32px;
|
||||||
border-inline-start: 3px solid $darker-text-color;
|
color: var(--rich-text-text-color);
|
||||||
color: $darker-text-color;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
p:last-child {
|
&::before {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 20px;
|
||||||
|
mask-image: url('~images/quote.svg');
|
||||||
|
background-color: var(--rich-text-decorations-color);
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-top: 4px;
|
||||||
|
border-inline-start: 3px solid var(--rich-text-decorations-color);
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul,
|
& > ul,
|
||||||
& > ol {
|
& > ol {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -76,7 +122,15 @@
|
|||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
margin-inline-start: 2em;
|
padding-inline-start: 24px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -84,7 +138,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: '•';
|
||||||
|
|
||||||
|
li::marker {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
|
@ -90,6 +90,10 @@ body.rtl {
|
|||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-back-button__icon {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.simple_form select {
|
.simple_form select {
|
||||||
background: $ui-base-color
|
background: $ui-base-color
|
||||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")
|
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")
|
||||||
|
@ -122,4 +122,7 @@ $dismiss-overlay-width: 4rem;
|
|||||||
--error-background-color: #{darken($error-red, 16%)};
|
--error-background-color: #{darken($error-red, 16%)};
|
||||||
--error-active-background-color: #{darken($error-red, 12%)};
|
--error-active-background-color: #{darken($error-red, 12%)};
|
||||||
--on-error-color: #fff;
|
--on-error-color: #fff;
|
||||||
|
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||||
|
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||||
}
|
}
|
||||||
|
BIN
app/javascript/images/archetypes/booster.png
Executable file
BIN
app/javascript/images/archetypes/booster.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 620 KiB |
BIN
app/javascript/images/archetypes/lurker.png
Executable file
BIN
app/javascript/images/archetypes/lurker.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
app/javascript/images/archetypes/oracle.png
Executable file
BIN
app/javascript/images/archetypes/oracle.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user