mirror of
https://github.com/glitch-soc/mastodon
synced 2025-04-24 15:24:51 +00:00
Merge commit '04492e7f934d07f8e89fa9c3d4fe3381f251e8a2' into glitch-soc/merge-upstream
This commit is contained in:
commit
4bea9a0a66
83 changed files with 1520 additions and 846 deletions
|
@ -20,3 +20,9 @@ postgres14
|
||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
|
.yarn/
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
1
.github/workflows/test-image-build.yml
vendored
1
.github/workflows/test-image-build.yml
vendored
|
@ -8,6 +8,7 @@ on:
|
||||||
- .github/workflows/test-image-build.yml
|
- .github/workflows/test-image-build.yml
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
- streaming/Dockerfile
|
- streaming/Dockerfile
|
||||||
|
- .dockerignore
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
12
.github/workflows/test-migrations.yml
vendored
12
.github/workflows/test-migrations.yml
vendored
|
@ -77,6 +77,18 @@ jobs:
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
|
- name: Ensure no errors with `db:prepare`
|
||||||
|
run: |
|
||||||
|
bin/rails db:drop
|
||||||
|
bin/rails db:prepare
|
||||||
|
bin/rails db:migrate
|
||||||
|
|
||||||
|
- name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS
|
||||||
|
run: |
|
||||||
|
bin/rails db:drop
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare
|
||||||
|
bin/rails db:migrate
|
||||||
|
|
||||||
- name: Test "one step migration" flow
|
- name: Test "one step migration" flow
|
||||||
run: |
|
run: |
|
||||||
bin/rails db:drop
|
bin/rails db:drop
|
||||||
|
|
|
@ -63,6 +63,7 @@ docker-compose.override.yml
|
||||||
|
|
||||||
# Ignore emoji map file
|
# Ignore emoji map file
|
||||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||||
|
/app/javascript/mastodon/features/emoji/emoji_sheet.json
|
||||||
|
|
||||||
# Ignore locale files
|
# Ignore locale files
|
||||||
/app/javascript/mastodon/locales/*.json
|
/app/javascript/mastodon/locales/*.json
|
||||||
|
|
|
@ -96,6 +96,9 @@ RUN \
|
||||||
# Set /opt/mastodon as working directory
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
|
# Add backport repository for some specific packages where we need the latest version
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||||
|
|
||||||
# hadolint ignore=DL3008,DL3005
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN \
|
RUN \
|
||||||
# Mount Apt cache and lib directories from Docker buildx caches
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
|
@ -165,7 +168,7 @@ RUN \
|
||||||
libexif-dev \
|
libexif-dev \
|
||||||
libexpat1-dev \
|
libexpat1-dev \
|
||||||
libgirepository1.0-dev \
|
libgirepository1.0-dev \
|
||||||
libheif-dev \
|
libheif-dev/bookworm-backports \
|
||||||
libimagequant-dev \
|
libimagequant-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
liblcms2-dev \
|
liblcms2-dev \
|
||||||
|
@ -348,7 +351,7 @@ RUN \
|
||||||
# libvips components
|
# libvips components
|
||||||
libcgif0 \
|
libcgif0 \
|
||||||
libexif12 \
|
libexif12 \
|
||||||
libheif1 \
|
libheif1/bookworm-backports \
|
||||||
libimagequant0 \
|
libimagequant0 \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
liblcms2-2 \
|
liblcms2-2 \
|
||||||
|
|
6
Gemfile
6
Gemfile
|
@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
|
||||||
|
|
||||||
gem 'omniauth', '~> 2.0'
|
gem 'omniauth', '~> 2.0'
|
||||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||||
gem 'omniauth_openid_connect', '~> 0.6.1'
|
gem 'omniauth_openid_connect', '~> 0.8.0'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
gem 'omniauth-saml', '~> 2.0'
|
gem 'omniauth-saml', '~> 2.0'
|
||||||
|
|
||||||
|
@ -102,10 +102,10 @@ gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
gem 'prometheus_exporter', '~> 2.2', require: false
|
gem 'prometheus_exporter', '~> 2.2', require: false
|
||||||
|
|
||||||
gem 'opentelemetry-api', '~> 1.4.0'
|
gem 'opentelemetry-api', '~> 1.5.0'
|
||||||
|
|
||||||
group :opentelemetry do
|
group :opentelemetry do
|
||||||
gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
|
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
||||||
|
|
80
Gemfile.lock
80
Gemfile.lock
|
@ -217,6 +217,8 @@ GEM
|
||||||
htmlentities (~> 4.3.3)
|
htmlentities (~> 4.3.3)
|
||||||
launchy (>= 2.1, < 4.0)
|
launchy (>= 2.1, < 4.0)
|
||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
|
email_validator (2.2.4)
|
||||||
|
activemodel
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
@ -228,6 +230,8 @@ GEM
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
|
faraday-follow_redirects (0.3.0)
|
||||||
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (2.0.1)
|
faraday-httpclient (2.0.1)
|
||||||
httpclient (>= 2.2)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (3.4.0)
|
faraday-net_http (3.4.0)
|
||||||
|
@ -261,8 +265,8 @@ GEM
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (3.25.5)
|
google-protobuf (3.25.6)
|
||||||
googleapis-common-protos-types (1.15.0)
|
googleapis-common-protos-types (1.18.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
haml (6.3.0)
|
haml (6.3.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
|
@ -330,11 +334,13 @@ GEM
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.10.1)
|
json (2.10.1)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.16.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
|
base64
|
||||||
bindata
|
bindata
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-ld (3.3.2)
|
json-ld (3.3.2)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 1.0)
|
json-canonicalization (~> 1.0)
|
||||||
|
@ -435,41 +441,43 @@ GEM
|
||||||
oj (3.16.10)
|
oj (3.16.10)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.3)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.0)
|
omniauth-cas (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
omniauth-rails_csrf_protection (1.0.2)
|
omniauth-rails_csrf_protection (1.0.2)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-saml (2.2.1)
|
omniauth-saml (2.2.2)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
ruby-saml (~> 1.17)
|
ruby-saml (~> 1.17)
|
||||||
omniauth_openid_connect (0.6.1)
|
omniauth_openid_connect (0.8.0)
|
||||||
omniauth (>= 1.9, < 3)
|
omniauth (>= 1.9, < 3)
|
||||||
openid_connect (~> 1.1)
|
openid_connect (~> 2.2)
|
||||||
openid_connect (1.4.2)
|
openid_connect (2.3.1)
|
||||||
activemodel
|
activemodel
|
||||||
attr_required (>= 1.0.0)
|
attr_required (>= 1.0.0)
|
||||||
json-jwt (>= 1.15.0)
|
email_validator
|
||||||
net-smtp
|
faraday (~> 2.0)
|
||||||
rack-oauth2 (~> 1.21)
|
faraday-follow_redirects
|
||||||
swd (~> 1.3)
|
json-jwt (>= 1.16)
|
||||||
|
mail
|
||||||
|
rack-oauth2 (~> 2.2)
|
||||||
|
swd (~> 2.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
validate_email
|
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (~> 1.2)
|
webfinger (~> 2.0)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
opentelemetry-api (1.4.0)
|
opentelemetry-api (1.5.0)
|
||||||
opentelemetry-common (0.21.0)
|
opentelemetry-common (0.22.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-exporter-otlp (0.29.1)
|
opentelemetry-exporter-otlp (0.30.0)
|
||||||
google-protobuf (>= 3.18)
|
google-protobuf (>= 3.18)
|
||||||
googleapis-common-protos-types (~> 1.3)
|
googleapis-common-protos-types (~> 1.3)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
|
@ -500,8 +508,8 @@ GEM
|
||||||
opentelemetry-instrumentation-active_record (0.9.0)
|
opentelemetry-instrumentation-active_record (0.9.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-active_storage (0.1.0)
|
opentelemetry-instrumentation-active_storage (0.1.1)
|
||||||
opentelemetry-api (~> 1.4.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.7)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-active_support (0.8.0)
|
opentelemetry-instrumentation-active_support (0.8.0)
|
||||||
|
@ -553,14 +561,14 @@ GEM
|
||||||
opentelemetry-instrumentation-sidekiq (0.26.0)
|
opentelemetry-instrumentation-sidekiq (0.26.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-registry (0.3.1)
|
opentelemetry-registry (0.4.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-sdk (1.7.0)
|
opentelemetry-sdk (1.8.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-registry (~> 0.2)
|
opentelemetry-registry (~> 0.2)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-semantic_conventions (1.10.1)
|
opentelemetry-semantic_conventions (1.11.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
|
@ -609,10 +617,11 @@ GEM
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-cors (2.0.2)
|
rack-cors (2.0.2)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-oauth2 (1.21.3)
|
rack-oauth2 (2.2.1)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (3.2.0)
|
rack-protection (3.2.0)
|
||||||
|
@ -816,10 +825,11 @@ GEM
|
||||||
stringio (3.1.4)
|
stringio (3.1.4)
|
||||||
strong_migrations (2.2.0)
|
strong_migrations (2.2.0)
|
||||||
activerecord (>= 7)
|
activerecord (>= 7)
|
||||||
swd (1.3.0)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.3)
|
temple (0.10.3)
|
||||||
terminal-table (4.0.0)
|
terminal-table (4.0.0)
|
||||||
|
@ -859,9 +869,6 @@ GEM
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.0.4)
|
||||||
uri (1.0.3)
|
uri (1.0.3)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
validate_email (0.1.6)
|
|
||||||
activemodel (>= 3.0)
|
|
||||||
mail (>= 2.2.5)
|
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
|
@ -875,9 +882,10 @@ GEM
|
||||||
openssl (>= 2.2)
|
openssl (>= 2.2)
|
||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
tpm-key_attestation (~> 0.14.0)
|
tpm-key_attestation (~> 0.14.0)
|
||||||
webfinger (1.2.0)
|
webfinger (2.1.3)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
webmock (3.25.0)
|
webmock (3.25.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
|
@ -976,9 +984,9 @@ DEPENDENCIES
|
||||||
omniauth-cas (~> 3.0.0.beta.1)
|
omniauth-cas (~> 3.0.0.beta.1)
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.6.1)
|
omniauth_openid_connect (~> 0.8.0)
|
||||||
opentelemetry-api (~> 1.4.0)
|
opentelemetry-api (~> 1.5.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.29.0)
|
opentelemetry-exporter-otlp (~> 0.30.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::DistributionsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
@announcement.touch(:notification_sent_at)
|
||||||
|
Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
|
||||||
|
redirect_to admin_announcements_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
16
app/controllers/admin/announcements/previews_controller.rb
Normal file
16
app/controllers/admin/announcements/previews_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::PreviewsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
@user_count = @announcement.scope_for_notification.count
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
17
app/controllers/admin/announcements/tests_controller.rb
Normal file
17
app/controllers/admin/announcements/tests_controller.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Announcements::TestsController < Admin::BaseController
|
||||||
|
before_action :set_announcement
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @announcement, :distribute?
|
||||||
|
UserMailer.announcement_published(current_user, @announcement).deliver_later!
|
||||||
|
redirect_to admin_announcements_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_announcement
|
||||||
|
@announcement = Announcement.find(params[:announcement_id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
<Sparklines
|
<Sparklines
|
||||||
width={50}
|
width={50}
|
||||||
height={28}
|
height={28}
|
||||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||||
>
|
>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
|
|
|
@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const MediaItem: React.FC<{
|
||||||
attachment.get('description')) as string | undefined;
|
attachment.get('description')) as string | undefined;
|
||||||
const previewUrl = attachment.get('preview_url') as string;
|
const previewUrl = attachment.get('preview_url') as string;
|
||||||
const fullUrl = attachment.get('url') as string;
|
const fullUrl = attachment.get('url') as string;
|
||||||
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
|
const avatarUrl = account?.avatar_static;
|
||||||
const lang = status.get('language') as string;
|
const lang = status.get('language') as string;
|
||||||
const blurhash = attachment.get('blurhash') as string;
|
const blurhash = attachment.get('blurhash') as string;
|
||||||
const statusId = status.get('id') as string;
|
const statusId = status.get('id') as string;
|
||||||
|
|
|
@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
|
|
||||||
|
const nimblePickerData = emojiCompressed[5];
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||||
|
@ -37,15 +40,18 @@ let EmojiPicker, Emoji; // load asynchronously
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||||
|
|
||||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||||
|
|
||||||
const notFoundFn = () => (
|
const notFoundFn = () => (
|
||||||
<div className='emoji-mart-no-results'>
|
<div className='emoji-mart-no-results'>
|
||||||
<Emoji
|
<Emoji
|
||||||
|
data={nimblePickerData}
|
||||||
emoji='sleuth_or_spy'
|
emoji='sleuth_or_spy'
|
||||||
set='twitter'
|
set='twitter'
|
||||||
size={32}
|
size={32}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.active) {
|
if (nextProps.active) {
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
} else {
|
} else {
|
||||||
|
@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
attachListeners () {
|
attachListeners() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListeners () {
|
removeListeners() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active } = this.props;
|
const { active } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||||
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active, modifier } = this.props;
|
const { active, modifier } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers'>
|
<div className='emoji-picker-dropdown__modifiers'>
|
||||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
<Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
@ -199,7 +205,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
this.props.onSkinTone(modifier);
|
this.props.onSkinTone(modifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
data={nimblePickerData}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
perLine={8}
|
perLine={8}
|
||||||
emojiSize={22}
|
emojiSize={22}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
@ -345,7 +354,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
|
|
||||||
EmojiPickerAsync().then(EmojiMart => {
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
EmojiPicker = EmojiMart.Picker;
|
EmojiPicker = EmojiMart.Picker;
|
||||||
Emoji = EmojiMart.Emoji;
|
Emoji = EmojiMart.Emoji;
|
||||||
|
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -386,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
this.setState({ placement: state.placement });
|
this.setState({ placement: state.placement });
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
const { active, loading, placement } = this.state;
|
const { active, loading, placement } = this.state;
|
||||||
|
@ -403,7 +412,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||||
{({ props, placement })=> (
|
{({ props, placement }) => (
|
||||||
<div {...props} style={{ ...props.style }}>
|
<div {...props} style={{ ...props.style }}>
|
||||||
<div className={`dropdown-animation ${placement}`}>
|
<div className={`dropdown-animation ${placement}`}>
|
||||||
<EmojiPickerMenu
|
<EmojiPickerMenu
|
||||||
|
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
||||||
Category[],
|
Category[],
|
||||||
Data['aliases'],
|
Data['aliases'],
|
||||||
EmojisWithoutShortCodes,
|
EmojisWithoutShortCodes,
|
||||||
|
Data,
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -9,18 +9,91 @@
|
||||||
|
|
||||||
// This version comment should be bumped each time the emoji data is changed
|
// This version comment should be bumped each time the emoji data is changed
|
||||||
// to ensure that the prevaled file is regenerated by Babel
|
// to ensure that the prevaled file is regenerated by Babel
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const { emojiIndex } = require('emoji-mart');
|
// This json file contains the names of the categories.
|
||||||
let data = require('emoji-mart/data/all.json');
|
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
|
||||||
|
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
|
||||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
const emojiMap = require('./emoji_map.json');
|
const emojiMap = require('./emoji_map.json');
|
||||||
|
// This json file is downloaded from https://github.com/iamcal/emoji-data/
|
||||||
|
// and is used to correct the sheet coordinates since we're using that repo's sheet
|
||||||
|
const emojiSheetData = require('./emoji_sheet.json');
|
||||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||||
|
|
||||||
if(data.compressed) {
|
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||||
data = emojiMartUncompress(data);
|
function unifiedToNative(unified) {
|
||||||
|
let unicodes = unified.split('-'),
|
||||||
|
codePoints = unicodes.map((u) => `0x${u}`);
|
||||||
|
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
compressed: true,
|
||||||
|
categories: emojiMart5Data.categories.map(cat => {
|
||||||
|
return {
|
||||||
|
...cat,
|
||||||
|
name: emojiMart5LocalesData.categories[cat.id]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
aliases: emojiMart5Data.aliases,
|
||||||
|
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
|
||||||
|
let skin_variations = {};
|
||||||
|
const unified = emoji.skins[0].unified.toUpperCase();
|
||||||
|
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
|
||||||
|
|
||||||
|
if (!emojiFromRawData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.skins.length > 1) {
|
||||||
|
const [, ...nonDefaultSkins] = emoji.skins;
|
||||||
|
nonDefaultSkins.forEach(skin => {
|
||||||
|
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
|
||||||
|
const [, value] = pair;
|
||||||
|
return value.unified.toLowerCase() === skin.unified;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingRawEmoji && matchingRawCodePoints) {
|
||||||
|
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
|
||||||
|
// for emoji like `woman-heart-woman` with two different skin tones.
|
||||||
|
const skinToneCode = matchingRawCodePoints.split('-')[0];
|
||||||
|
skin_variations[skinToneCode] = {
|
||||||
|
unified: matchingRawEmoji.unified.toUpperCase(),
|
||||||
|
non_qualified: null,
|
||||||
|
sheet_x: matchingRawEmoji.sheet_x,
|
||||||
|
sheet_y: matchingRawEmoji.sheet_y,
|
||||||
|
has_img_twitter: true,
|
||||||
|
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
a: emoji.name,
|
||||||
|
b: unified,
|
||||||
|
c: undefined,
|
||||||
|
f: true,
|
||||||
|
j: [emoji.id, ...emoji.keywords],
|
||||||
|
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
|
||||||
|
m: emoji.emoticons?.[0],
|
||||||
|
l: emoji.emoticons,
|
||||||
|
o: emoji.version,
|
||||||
|
id: emoji.id,
|
||||||
|
skin_variations,
|
||||||
|
native: unifiedToNative(unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.compressed) {
|
||||||
|
emojiMartUncompress(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojiMartData = data;
|
const emojiMartData = data;
|
||||||
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
||||||
const shortCodesToEmojiData = {};
|
const shortCodesToEmojiData = {};
|
||||||
const emojisWithoutShortCodes = [];
|
const emojisWithoutShortCodes = [];
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMart5Data.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcodeMap[emoji.native] = emoji.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripModifiers = unicode => {
|
const stripModifiers = unicode => {
|
||||||
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMartData.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMartData.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { native } = emoji;
|
const { native } = emoji;
|
||||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
||||||
emojiMartData.categories,
|
emojiMartData.categories,
|
||||||
emojiMartData.aliases,
|
emojiMartData.aliases,
|
||||||
emojisWithoutShortCodes,
|
emojisWithoutShortCodes,
|
||||||
|
emojiMartData
|
||||||
]));
|
]));
|
||||||
|
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
type Emojis = {
|
type Emojis = Record<
|
||||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
|
{
|
||||||
native: BaseEmoji['native'];
|
native: BaseEmoji['native'];
|
||||||
search: Search;
|
search: Search;
|
||||||
short_names: Emoji['short_names'];
|
short_names: Emoji['short_names'];
|
||||||
unified: Emoji['unified'];
|
unified: Emoji['unified'];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Picker,
|
Picker,
|
||||||
|
|
1
app/javascript/mastodon/features/emoji/emoji_sheet.json
Normal file
1
app/javascript/mastodon/features/emoji/emoji_sheet.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -9,12 +9,13 @@ import type {
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_to_filename';
|
||||||
|
|
||||||
type UnicodeMapping = {
|
type UnicodeMapping = Record<
|
||||||
[key in FilenameData[number][0]]: {
|
FilenameData[number][0],
|
||||||
|
{
|
||||||
shortCode: ShortCodesToEmojiDataKey;
|
shortCode: ShortCodesToEmojiDataKey;
|
||||||
filename: FilenameData[number][number];
|
filename: FilenameData[number][number];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(key: string, checked: boolean) => {
|
(key: string[], checked: boolean) => {
|
||||||
dispatch(changeSetting(['home', ...key], checked));
|
dispatch(changeSetting(['home', ...key], checked));
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
|
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
sandbox='allow-scripts allow-same-origin'
|
sandbox='allow-scripts allow-same-origin'
|
||||||
|
|
|
@ -242,12 +242,26 @@ const expiresInFromExpiresAt = expires_at => {
|
||||||
|
|
||||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||||
prefix = prefix.toLowerCase();
|
prefix = prefix.toLowerCase();
|
||||||
|
|
||||||
if (suggestions.length < 4) {
|
if (suggestions.length < 4) {
|
||||||
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
||||||
return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||||
} else {
|
|
||||||
return suggestions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer capitalization from personal history, unless personal history is all lower-case
|
||||||
|
const fixSuggestionCapitalization = (suggestion) => {
|
||||||
|
if (suggestion.type !== 'hashtag')
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0);
|
||||||
|
|
||||||
|
if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory)
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
return { ...suggestion, name: tagFromHistory };
|
||||||
|
};
|
||||||
|
|
||||||
|
return suggestions.map(fixSuggestionCapitalization);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
||||||
|
|
|
@ -219,6 +219,15 @@ class UserMailer < Devise::Mailer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def announcement_published(user, announcement)
|
||||||
|
@resource = user
|
||||||
|
@announcement = announcement
|
||||||
|
|
||||||
|
I18n.with_locale(locale) do
|
||||||
|
mail subject: default_i18n_subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def default_devise_subject
|
def default_devise_subject
|
||||||
|
|
|
@ -4,17 +4,18 @@
|
||||||
#
|
#
|
||||||
# Table name: announcements
|
# Table name: announcements
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# text :text default(""), not null
|
# all_day :boolean default(FALSE), not null
|
||||||
# published :boolean default(FALSE), not null
|
# ends_at :datetime
|
||||||
# all_day :boolean default(FALSE), not null
|
# notification_sent_at :datetime
|
||||||
# scheduled_at :datetime
|
# published :boolean default(FALSE), not null
|
||||||
# starts_at :datetime
|
# published_at :datetime
|
||||||
# ends_at :datetime
|
# scheduled_at :datetime
|
||||||
# created_at :datetime not null
|
# starts_at :datetime
|
||||||
# updated_at :datetime not null
|
# status_ids :bigint(8) is an Array
|
||||||
# published_at :datetime
|
# text :text default(""), not null
|
||||||
# status_ids :bigint(8) is an Array
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Announcement < ApplicationRecord
|
class Announcement < ApplicationRecord
|
||||||
|
@ -54,6 +55,10 @@ class Announcement < ApplicationRecord
|
||||||
update!(published: false, scheduled_at: nil)
|
update!(published: false, scheduled_at: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notification_sent?
|
||||||
|
notification_sent_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
def mentions
|
def mentions
|
||||||
@mentions ||= Account.from_text(text)
|
@mentions ||= Account.from_text(text)
|
||||||
end
|
end
|
||||||
|
@ -86,6 +91,10 @@ class Announcement < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def scope_for_notification
|
||||||
|
User.confirmed.joins(:account).merge(Account.without_suspended)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def grouped_ordered_announcement_reactions
|
def grouped_ordered_announcement_reactions
|
||||||
|
|
|
@ -9,11 +9,12 @@ class TermsOfService::Generator
|
||||||
admin_email
|
admin_email
|
||||||
arbitration_address
|
arbitration_address
|
||||||
arbitration_website
|
arbitration_website
|
||||||
|
choice_of_law
|
||||||
dmca_address
|
dmca_address
|
||||||
dmca_email
|
dmca_email
|
||||||
domain
|
domain
|
||||||
jurisdiction
|
jurisdiction
|
||||||
choice_of_law
|
min_age
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
attr_accessor(*VARIABLES)
|
attr_accessor(*VARIABLES)
|
||||||
|
|
|
@ -16,4 +16,8 @@ class AnnouncementPolicy < ApplicationPolicy
|
||||||
def destroy?
|
def destroy?
|
||||||
role.can?(:manage_announcements)
|
role.can?(:manage_announcements)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def distribute?
|
||||||
|
record.published? && !record.notification_sent? && role.can?(:manage_settings)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
= l(announcement.created_at)
|
= l(announcement.created_at)
|
||||||
|
|
||||||
%div
|
%div
|
||||||
|
- if can?(:distribute, announcement)
|
||||||
|
= table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement)
|
||||||
- if can?(:update, announcement)
|
- if can?(:update, announcement)
|
||||||
- if announcement.published?
|
- if announcement.published?
|
||||||
= table_link_to 'toggle_off', t('admin.announcements.unpublish'), unpublish_admin_announcement_path(announcement), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
|
= table_link_to 'toggle_off', t('admin.announcements.unpublish'), unpublish_admin_announcement_path(announcement), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||||
|
|
20
app/views/admin/announcements/previews/show.html.haml
Normal file
20
app/views/admin/announcements/previews/show.html.haml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.announcements.preview.title')
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
.back-link
|
||||||
|
= link_to admin_announcements_path do
|
||||||
|
= material_symbol 'chevron_left'
|
||||||
|
= t('admin.announcements.back')
|
||||||
|
|
||||||
|
%p.lead
|
||||||
|
= t('admin.announcements.preview.explanation_html', count: @user_count, display_count: number_with_delimiter(@user_count))
|
||||||
|
|
||||||
|
.prose
|
||||||
|
= linkify(@announcement.text)
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
.content__heading__actions
|
||||||
|
= link_to t('admin.terms_of_service.preview.send_preview', email: current_user.email), admin_announcement_test_path(@announcement), method: :post, class: 'button button-secondary'
|
||||||
|
= link_to t('admin.terms_of_service.preview.send_to_all', count: @user_count, display_count: number_with_delimiter(@user_count)), admin_announcement_distribution_path(@announcement), method: :post, class: 'button', data: { confirm: t('admin.reports.are_you_sure') }
|
|
@ -19,6 +19,9 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= form.input :domain, wrapper: :with_label
|
= form.input :domain, wrapper: :with_label
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= form.input :min_age, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= form.input :jurisdiction, wrapper: :with_label
|
= form.input :jurisdiction, wrapper: :with_label
|
||||||
|
|
||||||
|
|
12
app/views/user_mailer/announcement_published.html.haml
Normal file
12
app/views/user_mailer/announcement_published.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
= content_for :heading do
|
||||||
|
= render 'application/mailer/heading',
|
||||||
|
image_url: frontend_asset_url('images/mailer-new/heading/user.png'),
|
||||||
|
title: t('user_mailer.announcement_published.title', domain: site_hostname)
|
||||||
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
|
%tr
|
||||||
|
%td.email-body-padding-td
|
||||||
|
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
|
%tr
|
||||||
|
%td.email-inner-card-td.email-prose
|
||||||
|
%p= t('user_mailer.announcement_published.description', domain: site_hostname)
|
||||||
|
= linkify(@announcement.text)
|
7
app/views/user_mailer/announcement_published.text.erb
Normal file
7
app/views/user_mailer/announcement_published.text.erb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<%= t('user_mailer.announcement_published.title') %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t('user_mailer.announcement_published.description', domain: site_hostname) %>
|
||||||
|
|
||||||
|
<%= @announcement.text %>
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::DistributeAnnouncementNotificationWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def perform(announcement_id)
|
||||||
|
announcement = Announcement.find(announcement_id)
|
||||||
|
|
||||||
|
announcement.scope_for_notification.find_each do |user|
|
||||||
|
UserMailer.announcement_published(user, announcement).deliver_later!
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,14 +4,4 @@
|
||||||
# before other initializers as Rails may otherwise memoize a list of migrations
|
# before other initializers as Rails may otherwise memoize a list of migrations
|
||||||
# excluding the post deployment migrations.
|
# excluding the post deployment migrations.
|
||||||
|
|
||||||
unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
|
Mastodon::Database.add_post_migrate_path_to_rails
|
||||||
Rails.application.config.paths['db'].each do |db_path|
|
|
||||||
path = Rails.root.join(db_path, 'post_migrate').to_s
|
|
||||||
|
|
||||||
Rails.application.config.paths['db/migrate'] << path
|
|
||||||
|
|
||||||
# Rails memoizes migrations at certain points where it won't read the above
|
|
||||||
# path just yet. As such we must also update the following list of paths.
|
|
||||||
ActiveRecord::Migrator.migrations_paths << path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -309,6 +309,7 @@ en:
|
||||||
title: Audit log
|
title: Audit log
|
||||||
unavailable_instance: "(domain name unavailable)"
|
unavailable_instance: "(domain name unavailable)"
|
||||||
announcements:
|
announcements:
|
||||||
|
back: Back to announcements
|
||||||
destroyed_msg: Announcement successfully deleted!
|
destroyed_msg: Announcement successfully deleted!
|
||||||
edit:
|
edit:
|
||||||
title: Edit announcement
|
title: Edit announcement
|
||||||
|
@ -317,6 +318,9 @@ en:
|
||||||
new:
|
new:
|
||||||
create: Create announcement
|
create: Create announcement
|
||||||
title: New announcement
|
title: New announcement
|
||||||
|
preview:
|
||||||
|
explanation_html: 'The email will be sent to <strong>%{display_count} users</strong>. The following text will be included in the e-mail:'
|
||||||
|
title: Preview announcement notification
|
||||||
publish: Publish
|
publish: Publish
|
||||||
published_msg: Announcement successfully published!
|
published_msg: Announcement successfully published!
|
||||||
scheduled_for: Scheduled for %{time}
|
scheduled_for: Scheduled for %{time}
|
||||||
|
@ -1906,6 +1910,10 @@ en:
|
||||||
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
|
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
|
||||||
webauthn: Security keys
|
webauthn: Security keys
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
announcement_published:
|
||||||
|
description: 'The administrators of %{domain} are making an announcement:'
|
||||||
|
subject: Service announcement
|
||||||
|
title: "%{domain} service announcement"
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Account Settings
|
action: Account Settings
|
||||||
explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
|
explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
|
||||||
|
|
|
@ -136,13 +136,14 @@ en:
|
||||||
text: Can be structured with Markdown syntax.
|
text: Can be structured with Markdown syntax.
|
||||||
terms_of_service_generator:
|
terms_of_service_generator:
|
||||||
admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
|
admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
|
||||||
arbitration_address: Can be the same as Physical address above, or “N/A” if using email
|
arbitration_address: Can be the same as Physical address above, or “N/A” if using email.
|
||||||
arbitration_website: Can be a web form, or “N/A” if using email
|
arbitration_website: Can be a web form, or “N/A” if using email.
|
||||||
choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims.
|
choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims.
|
||||||
dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
|
dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
|
||||||
dmca_email: Can be the same email used for “Email address for legal notices” above
|
dmca_email: Can be the same email used for “Email address for legal notices” above.
|
||||||
domain: Unique identification of the online service you are providing.
|
domain: Unique identification of the online service you are providing.
|
||||||
jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate.
|
jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate.
|
||||||
|
min_age: Should not be below the minimum age required by the laws of your jurisdiction.
|
||||||
user:
|
user:
|
||||||
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
||||||
role: The role controls which permissions the user has.
|
role: The role controls which permissions the user has.
|
||||||
|
@ -346,6 +347,7 @@ en:
|
||||||
dmca_email: Email address for DMCA/copyright notices
|
dmca_email: Email address for DMCA/copyright notices
|
||||||
domain: Domain
|
domain: Domain
|
||||||
jurisdiction: Legal jurisdiction
|
jurisdiction: Legal jurisdiction
|
||||||
|
min_age: Minimum age
|
||||||
user:
|
user:
|
||||||
role: Role
|
role: Role
|
||||||
time_zone: Time zone
|
time_zone: Time zone
|
||||||
|
|
|
@ -50,6 +50,10 @@ namespace :admin do
|
||||||
post :publish
|
post :publish
|
||||||
post :unpublish
|
post :unpublish
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resource :preview, only: [:show], module: :announcements
|
||||||
|
resource :test, only: [:create], module: :announcements
|
||||||
|
resource :distribution, only: [:create], module: :announcements
|
||||||
end
|
end
|
||||||
|
|
||||||
with_options to: redirect('/admin/settings/branding') do
|
with_options to: redirect('/admin/settings/branding') do
|
||||||
|
|
|
@ -17,7 +17,7 @@ into these Terms. You should also read these policies before using the Instance.
|
||||||
|
|
||||||
## Age Requirements and Responsibility of Parents and Legal Guardians
|
## Age Requirements and Responsibility of Parents and Legal Guardians
|
||||||
|
|
||||||
By accessing the Instance, you signify that you are at least thirteen years old
|
By accessing the Instance, you signify that you are at least %{min_age} years old
|
||||||
and that you meet the minimum age required by the laws in your country. If you
|
and that you meet the minimum age required by the laws in your country. If you
|
||||||
are old enough to access the Instance in your country, but are not old enough to
|
are old enough to access the Instance in your country, but are not old enough to
|
||||||
have the legal authority to consent to our Terms, please ask your parent or
|
have the legal authority to consent to our Terms, please ask your parent or
|
||||||
|
|
|
@ -48,7 +48,7 @@ module.exports = merge(sharedConfig, {
|
||||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
||||||
}),
|
}),
|
||||||
new InjectManifest({
|
new InjectManifest({
|
||||||
additionalManifestEntries: ['1f602.svg', 'sheet_13.png'].map((filename) => {
|
additionalManifestEntries: ['1f602.svg', 'sheet_15.png'].map((filename) => {
|
||||||
const path = resolve(root, 'public', 'emoji', filename);
|
const path = resolve(root, 'public', 'emoji', filename);
|
||||||
const body = readFileSync(path);
|
const body = readFileSync(path);
|
||||||
const md5 = createHash('md5');
|
const md5 = createHash('md5');
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddNotificationSentAtToAnnouncements < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :announcements, :notification_sent_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -258,6 +258,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.datetime "published_at", precision: nil
|
t.datetime "published_at", precision: nil
|
||||||
t.bigint "status_ids", array: true
|
t.bigint "status_ids", array: true
|
||||||
|
t.datetime "notification_sent_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "annual_report_statuses_per_account_counts", force: :cascade do |t|
|
create_table "annual_report_statuses_per_account_counts", force: :cascade do |t|
|
||||||
|
@ -1114,8 +1115,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.date "effective_date"
|
t.date "effective_date"
|
||||||
t.index ["effective_date"], name: "index_terms_of_services_on_effective_dat
|
t.index ["effective_date"], name: "index_terms_of_services_on_effective_date", unique: true, where: "(effective_date IS NOT NULL)"
|
||||||
e", unique: true, where: "(effective_date IS NOT NULL)"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tombstones", force: :cascade do |t|
|
create_table "tombstones", force: :cascade do |t|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../mastodon/database'
|
||||||
require_relative '../mastodon/snowflake'
|
require_relative '../mastodon/snowflake'
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
|
@ -8,6 +9,8 @@ module ActiveRecord
|
||||||
original_load_schema = instance_method(:load_schema)
|
original_load_schema = instance_method(:load_schema)
|
||||||
|
|
||||||
define_method(:load_schema) do |db_config, *args|
|
define_method(:load_schema) do |db_config, *args|
|
||||||
|
Mastodon::Database.add_post_migrate_path_to_rails(force: true)
|
||||||
|
|
||||||
ActiveRecord::Base.establish_connection(db_config)
|
ActiveRecord::Base.establish_connection(db_config)
|
||||||
Mastodon::Snowflake.define_timestamp_id
|
Mastodon::Snowflake.define_timestamp_id
|
||||||
|
|
||||||
|
|
46
lib/mastodon/database.rb
Normal file
46
lib/mastodon/database.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This file is entirely lifted from GitLab.
|
||||||
|
|
||||||
|
# The original file:
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/blob/69127d59467185cf4ff1109d88ceec1f499f354f/lib/gitlab/database.rb#L244-258
|
||||||
|
|
||||||
|
# Copyright (c) 2011-present GitLab B.V.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
module Mastodon
|
||||||
|
module Database
|
||||||
|
def self.add_post_migrate_path_to_rails(force: false)
|
||||||
|
return if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && !force
|
||||||
|
|
||||||
|
Rails.application.config.paths['db'].each do |db_path|
|
||||||
|
path = Rails.root.join(db_path, 'post_migrate').to_s
|
||||||
|
|
||||||
|
next if Rails.application.config.paths['db/migrate'].include?(path)
|
||||||
|
|
||||||
|
Rails.application.config.paths['db/migrate'] << path
|
||||||
|
|
||||||
|
# Rails memoizes migrations at certain points where it won't read the above
|
||||||
|
# path just yet. As such we must also update the following list of paths.
|
||||||
|
ActiveRecord::Migrator.migrations_paths << path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -103,4 +103,36 @@ namespace :emojis do
|
||||||
gen_border map[emoji], 'white'
|
gen_border map[emoji], 'white'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Download the JSON sheet data of emojis'
|
||||||
|
task :download_sheet_json do
|
||||||
|
source = 'https://raw.githubusercontent.com/iamcal/emoji-data/refs/tags/v15.1.2/emoji.json'
|
||||||
|
dest = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_sheet.json')
|
||||||
|
|
||||||
|
puts "Downloading emoji data from source... (#{source})"
|
||||||
|
|
||||||
|
res = HTTP.get(source).to_s
|
||||||
|
data = JSON.parse(res)
|
||||||
|
|
||||||
|
filtered_data = data.map do |emoji|
|
||||||
|
filtered_item = {
|
||||||
|
'unified' => emoji['unified'],
|
||||||
|
'sheet_x' => emoji['sheet_x'],
|
||||||
|
'sheet_y' => emoji['sheet_y'],
|
||||||
|
'skin_variations' => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
emoji['skin_variations']&.each do |key, variation|
|
||||||
|
filtered_item['skin_variations'][key] = {
|
||||||
|
'unified' => variation['unified'],
|
||||||
|
'sheet_x' => variation['sheet_x'],
|
||||||
|
'sheet_y' => variation['sheet_y'],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
filtered_item
|
||||||
|
end
|
||||||
|
|
||||||
|
File.write(dest, JSON.generate(filtered_data))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/mastodon",
|
"name": "@mastodon/mastodon",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.7.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@emoji-mart/data": "1.2.1",
|
||||||
"@formatjs/intl-pluralrules": "^5.2.2",
|
"@formatjs/intl-pluralrules": "^5.2.2",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^2.1.1",
|
"@github/webauthn-json": "^2.1.1",
|
||||||
|
@ -183,10 +184,10 @@
|
||||||
"eslint-define-config": "^2.0.0",
|
"eslint-define-config": "^2.0.0",
|
||||||
"eslint-import-resolver-typescript": "^3.5.5",
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
"eslint-plugin-formatjs": "^5.0.0",
|
"eslint-plugin-formatjs": "^5.0.0",
|
||||||
"eslint-plugin-import": "~2.30.0",
|
"eslint-plugin-import": "~2.31.0",
|
||||||
"eslint-plugin-jsdoc": "^50.0.0",
|
"eslint-plugin-jsdoc": "^50.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "~6.10.0",
|
"eslint-plugin-jsx-a11y": "~6.10.0",
|
||||||
"eslint-plugin-promise": "~7.1.0",
|
"eslint-plugin-promise": "~7.2.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB |
BIN
public/emoji/sheet_15.png
Normal file
BIN
public/emoji/sheet_15.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
|
@ -1,33 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Settings::SessionsController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:session_activation) { Fabricate(:session_activation, user: user) }
|
|
||||||
|
|
||||||
before { sign_in user, scope: :user }
|
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
|
||||||
subject { delete :destroy, params: { id: id } }
|
|
||||||
|
|
||||||
context 'when session activation exists' do
|
|
||||||
let(:id) { session_activation.id }
|
|
||||||
|
|
||||||
it 'destroys session activation' do
|
|
||||||
expect(subject).to redirect_to edit_user_registration_path
|
|
||||||
expect(SessionActivation.find_by(id: id)).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when session activation does not exist' do
|
|
||||||
let(:id) { session_activation.id + 1000 }
|
|
||||||
|
|
||||||
it 'destroys session activation' do
|
|
||||||
expect(subject).to have_http_status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -317,4 +317,16 @@ RSpec.describe UserMailer do
|
||||||
.and(have_body_text(I18n.t('user_mailer.terms_of_service_changed.changelog')))
|
.and(have_body_text(I18n.t('user_mailer.terms_of_service_changed.changelog')))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#announcement_published' do
|
||||||
|
let(:announcement) { Fabricate :announcement }
|
||||||
|
let(:mail) { described_class.announcement_published(receiver, announcement) }
|
||||||
|
|
||||||
|
it 'renders announcement_published mail' do
|
||||||
|
expect(mail)
|
||||||
|
.to be_present
|
||||||
|
.and(have_subject(I18n.t('user_mailer.announcement_published.subject')))
|
||||||
|
.and(have_body_text(I18n.t('user_mailer.announcement_published.description', domain: Rails.configuration.x.local_domain)))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
20
spec/requests/settings/sessions_spec.rb
Normal file
20
spec/requests/settings/sessions_spec.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Settings Sessions' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
describe 'DELETE /settings/sessions/:id' do
|
||||||
|
context 'when session activation does not exist' do
|
||||||
|
it 'returns not found' do
|
||||||
|
delete settings_session_path(123_456_789)
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,10 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SystemHelpers
|
module SystemHelpers
|
||||||
def admin_user
|
|
||||||
Fabricate(:admin_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def submit_button
|
def submit_button
|
||||||
I18n.t('generic.save_changes')
|
I18n.t('generic.save_changes')
|
||||||
end
|
end
|
||||||
|
|
28
spec/system/admin/announcements/distributions_spec.rb
Normal file
28
spec/system/admin/announcements/distributions_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin Announcement Mail Distributions' do
|
||||||
|
let(:user) { Fabricate(:admin_user) }
|
||||||
|
let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
|
||||||
|
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
describe 'Sending an announcement notification', :inline_jobs do
|
||||||
|
it 'marks the announcement as notified and sends the email' do
|
||||||
|
visit admin_announcement_preview_path(announcement)
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.announcements.preview.title'))
|
||||||
|
|
||||||
|
emails = capture_emails do
|
||||||
|
expect { click_on I18n.t('admin.terms_of_service.preview.send_to_all', count: 1, display_count: 1) }
|
||||||
|
.to(change { announcement.reload.notification_sent_at })
|
||||||
|
end
|
||||||
|
expect(emails.first)
|
||||||
|
.to be_present
|
||||||
|
.and(deliver_to(user.email))
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.announcements.title'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
spec/system/admin/announcements/previews_spec.rb
Normal file
19
spec/system/admin/announcements/previews_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin Announcements Mail Previews' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
|
describe 'Viewing Announcements Mail previews' do
|
||||||
|
it 'shows the Announcement Mail preview page' do
|
||||||
|
visit admin_announcement_preview_path(announcement)
|
||||||
|
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.announcements.preview.title'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
spec/system/admin/announcements/tests_spec.rb
Normal file
25
spec/system/admin/announcements/tests_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin TermsOfService Tests' do
|
||||||
|
let(:user) { Fabricate(:admin_user) }
|
||||||
|
let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
|
||||||
|
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
describe 'Sending test Announcement email', :inline_jobs do
|
||||||
|
it 'generates the test email' do
|
||||||
|
visit admin_announcement_preview_path(announcement)
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.announcements.preview.title'))
|
||||||
|
|
||||||
|
emails = capture_emails { click_on I18n.t('admin.terms_of_service.preview.send_preview', email: user.email) }
|
||||||
|
expect(emails.first)
|
||||||
|
.to be_present
|
||||||
|
.and(deliver_to(user.email))
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.announcements.title'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -117,7 +117,7 @@ RSpec.describe 'Admin::Announcements' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def text_label
|
def text_label
|
||||||
I18n.t('simple_form.labels.announcement.text')
|
form_label('announcement.text')
|
||||||
end
|
end
|
||||||
|
|
||||||
def admin_user
|
def admin_user
|
||||||
|
|
|
@ -57,7 +57,7 @@ RSpec.describe 'Admin Invites' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_use_field
|
def max_use_field
|
||||||
I18n.t('simple_form.labels.defaults.max_uses')
|
form_label('defaults.max_uses')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin Relationships' do
|
RSpec.describe 'Admin Relationships' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
before { sign_in(admin_user) }
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
describe 'Viewing account relationships page' do
|
describe 'Viewing account relationships page' do
|
||||||
|
|
|
@ -65,7 +65,7 @@ RSpec.describe 'Admin Rules' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_form
|
def submit_form
|
||||||
click_on I18n.t('generic.save_changes')
|
click_on(submit_button)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::About' do
|
RSpec.describe 'Admin::Settings::About' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to about settings' do
|
it 'Saves changes to about settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_about_path
|
visit admin_settings_about_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.about.title'))
|
||||||
|
|
||||||
fill_in extended_description_field,
|
fill_in extended_description_field,
|
||||||
with: 'new site description'
|
with: 'new site description'
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::Appearance' do
|
RSpec.describe 'Admin::Settings::Appearance' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to appearance settings' do
|
it 'Saves changes to appearance settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_appearance_path
|
visit admin_settings_appearance_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.appearance.title'))
|
||||||
|
|
||||||
fill_in custom_css_field,
|
fill_in custom_css_field,
|
||||||
with: 'html { display: inline; }'
|
with: 'html { display: inline; }'
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::Branding' do
|
RSpec.describe 'Admin::Settings::Branding' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to branding settings' do
|
it 'Saves changes to branding settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_branding_path
|
visit admin_settings_branding_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.branding.title'))
|
||||||
|
|
||||||
fill_in short_description_field,
|
fill_in short_description_field,
|
||||||
with: 'new key value'
|
with: 'new key value'
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::ContentRetention' do
|
RSpec.describe 'Admin::Settings::ContentRetention' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to content retention settings' do
|
it 'Saves changes to content retention settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_content_retention_path
|
visit admin_settings_content_retention_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.content_retention.title'))
|
||||||
|
|
||||||
fill_in media_cache_retention_period_field,
|
fill_in media_cache_retention_period_field,
|
||||||
with: '2'
|
with: '2'
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::Discovery' do
|
RSpec.describe 'Admin::Settings::Discovery' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to discovery settings' do
|
it 'Saves changes to discovery settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_discovery_path
|
visit admin_settings_discovery_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.discovery.title'))
|
||||||
|
|
||||||
check trends_box
|
check trends_box
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin::Settings::Registrations' do
|
RSpec.describe 'Admin::Settings::Registrations' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
it 'Saves changes to registrations settings' do
|
it 'Saves changes to registrations settings' do
|
||||||
sign_in admin_user
|
|
||||||
visit admin_settings_registrations_path
|
visit admin_settings_registrations_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('admin.settings.registrations.title'))
|
||||||
|
|
||||||
select open_mode_option,
|
select open_mode_option,
|
||||||
from: registrations_mode_field
|
from: registrations_mode_field
|
||||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe 'Admin Tags' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_name_field
|
def display_name_field
|
||||||
I18n.t('simple_form.labels.defaults.display_name')
|
form_label('defaults.display_name')
|
||||||
end
|
end
|
||||||
|
|
||||||
def match_error_text
|
def match_error_text
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin TermsOfService Drafts' do
|
RSpec.describe 'Admin TermsOfService Drafts' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
before { sign_in(admin_user) }
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
describe 'Managing TOS drafts' do
|
describe 'Managing TOS drafts' do
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin TermsOfService Generates' do
|
RSpec.describe 'Admin TermsOfService Generates' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
before { sign_in(admin_user) }
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
describe 'Generating a TOS policy' do
|
describe 'Generating a TOS policy' do
|
||||||
|
@ -27,6 +29,8 @@ RSpec.describe 'Admin TermsOfService Generates' do
|
||||||
fill_in 'terms_of_service_generator_domain', with: 'host.example'
|
fill_in 'terms_of_service_generator_domain', with: 'host.example'
|
||||||
fill_in 'terms_of_service_generator_jurisdiction', with: 'Europe'
|
fill_in 'terms_of_service_generator_jurisdiction', with: 'Europe'
|
||||||
fill_in 'terms_of_service_generator_choice_of_law', with: 'New York'
|
fill_in 'terms_of_service_generator_choice_of_law', with: 'New York'
|
||||||
|
fill_in 'terms_of_service_generator_min_age', with: '16'
|
||||||
|
|
||||||
expect { submit_form }
|
expect { submit_form }
|
||||||
.to change(TermsOfService, :count).by(1)
|
.to change(TermsOfService, :count).by(1)
|
||||||
expect(page)
|
expect(page)
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin TermsOfService Previews' do
|
RSpec.describe 'Admin TermsOfService Previews' do
|
||||||
let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
|
let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
before { sign_in(admin_user) }
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ RSpec.describe 'Admin Warning Presets' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_form
|
def submit_form
|
||||||
click_on I18n.t('generic.save_changes')
|
click_on(submit_button)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ RSpec.describe 'Admin Webhooks' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_form
|
def submit_form
|
||||||
click_on I18n.t('generic.save_changes')
|
click_on(submit_button)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,6 @@ RSpec.describe 'Filters' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_title_field
|
def filter_title_field
|
||||||
I18n.t('simple_form.labels.defaults.title')
|
form_label('defaults.title')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,9 +71,9 @@ RSpec.describe 'Invites' do
|
||||||
|
|
||||||
def fill_invite_form
|
def fill_invite_form
|
||||||
select I18n.t('invites.max_uses', count: 100),
|
select I18n.t('invites.max_uses', count: 100),
|
||||||
from: I18n.t('simple_form.labels.defaults.max_uses')
|
from: form_label('defaults.max_uses')
|
||||||
select I18n.t("invites.expires_in.#{30.minutes.to_i}"),
|
select I18n.t("invites.expires_in.#{30.minutes.to_i}"),
|
||||||
from: I18n.t('simple_form.labels.defaults.expires_in')
|
from: form_label('defaults.expires_in')
|
||||||
check I18n.t('simple_form.labels.defaults.autofollow')
|
check form_label('defaults.autofollow')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -96,7 +96,7 @@ RSpec.describe 'Settings applications page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_form
|
def submit_form
|
||||||
click_on I18n.t('generic.save_changes')
|
click_on(submit_button)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -33,18 +33,18 @@ RSpec.describe 'Settings preferences appearance page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirm_delete_field
|
def confirm_delete_field
|
||||||
I18n.t('simple_form.labels.defaults.setting_delete_modal')
|
form_label('defaults.setting_delete_modal')
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirm_reblog_field
|
def confirm_reblog_field
|
||||||
I18n.t('simple_form.labels.defaults.setting_boost_modal')
|
form_label('defaults.setting_boost_modal')
|
||||||
end
|
end
|
||||||
|
|
||||||
def theme_selection_field
|
def theme_selection_field
|
||||||
I18n.t('simple_form.labels.defaults.setting_theme')
|
form_label('defaults.setting_theme')
|
||||||
end
|
end
|
||||||
|
|
||||||
def advanced_layout_field
|
def advanced_layout_field
|
||||||
I18n.t('simple_form.labels.defaults.setting_advanced_layout')
|
form_label('defaults.setting_advanced_layout')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,6 @@ RSpec.describe 'Settings preferences notifications page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications_follow_field
|
def notifications_follow_field
|
||||||
I18n.t('simple_form.labels.notification_emails.follow')
|
form_label('notification_emails.follow')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ RSpec.describe 'Settings preferences other page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_sensitive_field
|
def mark_sensitive_field
|
||||||
I18n.t('simple_form.labels.defaults.setting_default_sensitive')
|
form_label('defaults.setting_default_sensitive')
|
||||||
end
|
end
|
||||||
|
|
||||||
def language_field(key)
|
def language_field(key)
|
||||||
|
|
|
@ -27,7 +27,7 @@ RSpec.describe 'Settings Privacy' do
|
||||||
.to change { user.account.reload.discoverable }.to(true)
|
.to change { user.account.reload.discoverable }.to(true)
|
||||||
expect(page)
|
expect(page)
|
||||||
.to have_content(I18n.t('privacy.title'))
|
.to have_content(I18n.t('privacy.title'))
|
||||||
.and have_content(I18n.t('generic.changes_saved_msg'))
|
.and have_content(success_message)
|
||||||
expect(ActivityPub::UpdateDistributionWorker)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
.to have_received(:perform_async).with(user.account.id)
|
.to have_received(:perform_async).with(user.account.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,10 +28,10 @@ RSpec.describe 'Settings profile page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_name_field
|
def display_name_field
|
||||||
I18n.t('simple_form.labels.defaults.display_name')
|
form_label('defaults.display_name')
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar_field
|
def avatar_field
|
||||||
I18n.t('simple_form.labels.defaults.avatar')
|
form_label('defaults.avatar')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
25
spec/system/settings/sessions_spec.rb
Normal file
25
spec/system/settings/sessions_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Settings Sessions' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let!(:session_activation) { Fabricate(:session_activation, user: user) }
|
||||||
|
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
describe 'deleting a session' do
|
||||||
|
it 'deletes listed session activation from the auth page' do
|
||||||
|
visit edit_user_registration_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('settings.account_settings'))
|
||||||
|
|
||||||
|
expect { click_on(I18n.t('sessions.revoke')) }
|
||||||
|
.to change(SessionActivation, :count).by(-1)
|
||||||
|
expect { session_activation.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect(page)
|
||||||
|
.to have_content(I18n.t('sessions.revoke_success'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -44,6 +44,6 @@ RSpec.describe 'Settings verification page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribution_field
|
def attribution_field
|
||||||
I18n.t('simple_form.labels.account.attribution_domains')
|
form_label('account.attribution_domains')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Admin::DistributeAnnouncementNotificationWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'with missing record' do
|
||||||
|
it 'runs without error' do
|
||||||
|
expect { worker.perform(nil) }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid announcement' do
|
||||||
|
let(:announcement) { Fabricate(:announcement) }
|
||||||
|
let!(:user) { Fabricate :user, confirmed_at: 3.days.ago }
|
||||||
|
|
||||||
|
it 'sends the announcement via email', :inline_jobs do
|
||||||
|
emails = capture_emails { worker.perform(announcement.id) }
|
||||||
|
|
||||||
|
expect(emails.size)
|
||||||
|
.to eq(1)
|
||||||
|
expect(emails.first)
|
||||||
|
.to have_attributes(
|
||||||
|
to: [user.email],
|
||||||
|
subject: I18n.t('user_mailer.announcement_published.subject')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/streaming",
|
"name": "@mastodon/streaming",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.7.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue