Merge pull request #2987 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 04492e7f93
pull/2988/head
commit
d94aeb2ce2
|
@ -20,3 +20,9 @@ postgres14
|
|||
redis
|
||||
elasticsearch
|
||||
chart
|
||||
.yarn/
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
|
|
@ -8,6 +8,7 @@ on:
|
|||
- .github/workflows/test-image-build.yml
|
||||
- Dockerfile
|
||||
- streaming/Dockerfile
|
||||
- .dockerignore
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
@ -77,6 +77,18 @@ jobs:
|
|||
- name: Set up Ruby environment
|
||||
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
|
||||
run: |
|
||||
bin/rails db:drop
|
||||
|
|
|
@ -63,6 +63,7 @@ docker-compose.override.yml
|
|||
|
||||
# Ignore emoji map file
|
||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||
/app/javascript/mastodon/features/emoji/emoji_sheet.json
|
||||
|
||||
# Ignore locale files
|
||||
/app/javascript/mastodon/locales/*.json
|
||||
|
@ -86,6 +87,7 @@ AUTHORS.md
|
|||
|
||||
# Ignore glitch-soc emoji map file
|
||||
/app/javascript/flavours/glitch/features/emoji/emoji_map.json
|
||||
/app/javascript/flavours/glitch/features/emoji/emoji_sheet.json
|
||||
|
||||
# Ignore glitch-soc locale files
|
||||
/app/javascript/flavours/glitch/locales
|
||||
|
|
|
@ -96,6 +96,9 @@ RUN \
|
|||
# Set /opt/mastodon as working directory
|
||||
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
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
|
@ -165,7 +168,7 @@ RUN \
|
|||
libexif-dev \
|
||||
libexpat1-dev \
|
||||
libgirepository1.0-dev \
|
||||
libheif-dev \
|
||||
libheif-dev/bookworm-backports \
|
||||
libimagequant-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
liblcms2-dev \
|
||||
|
@ -348,7 +351,7 @@ RUN \
|
|||
# libvips components
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libheif1/bookworm-backports \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
|
|||
|
||||
gem 'omniauth', '~> 2.0'
|
||||
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-saml', '~> 2.0'
|
||||
|
||||
|
@ -102,10 +102,10 @@ gem 'rdf-normalize', '~> 0.5'
|
|||
|
||||
gem 'prometheus_exporter', '~> 2.2', require: false
|
||||
|
||||
gem 'opentelemetry-api', '~> 1.4.0'
|
||||
gem 'opentelemetry-api', '~> 1.5.0'
|
||||
|
||||
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_model_serializers', '~> 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)
|
||||
launchy (>= 2.1, < 4.0)
|
||||
mail (~> 2.7)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
|
@ -228,6 +230,8 @@ GEM
|
|||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (2.0.1)
|
||||
httpclient (>= 2.2)
|
||||
faraday-net_http (3.4.0)
|
||||
|
@ -261,8 +265,8 @@ GEM
|
|||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (3.25.5)
|
||||
googleapis-common-protos-types (1.15.0)
|
||||
google-protobuf (3.25.6)
|
||||
googleapis-common-protos-types (1.18.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
haml (6.3.0)
|
||||
temple (>= 0.8.2)
|
||||
|
@ -330,11 +334,13 @@ GEM
|
|||
jmespath (1.6.2)
|
||||
json (2.10.1)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3.1)
|
||||
json-jwt (1.16.7)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
base64
|
||||
bindata
|
||||
httpclient
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-ld (3.3.2)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 1.0)
|
||||
|
@ -435,41 +441,43 @@ GEM
|
|||
oj (3.16.10)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.2)
|
||||
omniauth (2.1.3)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-cas (3.0.0)
|
||||
omniauth-cas (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.2.1)
|
||||
omniauth-saml (2.2.2)
|
||||
omniauth (~> 2.1)
|
||||
ruby-saml (~> 1.17)
|
||||
omniauth_openid_connect (0.6.1)
|
||||
omniauth_openid_connect (0.8.0)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 1.1)
|
||||
openid_connect (1.4.2)
|
||||
openid_connect (~> 2.2)
|
||||
openid_connect (2.3.1)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
json-jwt (>= 1.15.0)
|
||||
net-smtp
|
||||
rack-oauth2 (~> 1.21)
|
||||
swd (~> 1.3)
|
||||
email_validator
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.16)
|
||||
mail
|
||||
rack-oauth2 (~> 2.2)
|
||||
swd (~> 2.0)
|
||||
tzinfo
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (~> 1.2)
|
||||
webfinger (~> 2.0)
|
||||
openssl (3.3.0)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.4.0)
|
||||
opentelemetry-common (0.21.0)
|
||||
opentelemetry-api (1.5.0)
|
||||
opentelemetry-common (0.22.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-exporter-otlp (0.29.1)
|
||||
opentelemetry-exporter-otlp (0.30.0)
|
||||
google-protobuf (>= 3.18)
|
||||
googleapis-common-protos-types (~> 1.3)
|
||||
opentelemetry-api (~> 1.1)
|
||||
|
@ -500,8 +508,8 @@ GEM
|
|||
opentelemetry-instrumentation-active_record (0.9.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||
opentelemetry-instrumentation-active_storage (0.1.0)
|
||||
opentelemetry-api (~> 1.4.0)
|
||||
opentelemetry-instrumentation-active_storage (0.1.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||
opentelemetry-instrumentation-active_support (0.8.0)
|
||||
|
@ -553,14 +561,14 @@ GEM
|
|||
opentelemetry-instrumentation-sidekiq (0.26.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||
opentelemetry-registry (0.3.1)
|
||||
opentelemetry-registry (0.4.0)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-sdk (1.7.0)
|
||||
opentelemetry-sdk (1.8.0)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-common (~> 0.20)
|
||||
opentelemetry-registry (~> 0.2)
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-semantic_conventions (1.10.1)
|
||||
opentelemetry-semantic_conventions (1.11.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.1)
|
||||
|
@ -609,10 +617,11 @@ GEM
|
|||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-oauth2 (1.21.3)
|
||||
rack-oauth2 (2.2.1)
|
||||
activesupport
|
||||
attr_required
|
||||
httpclient
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (3.2.0)
|
||||
|
@ -816,10 +825,11 @@ GEM
|
|||
stringio (3.1.4)
|
||||
strong_migrations (2.2.0)
|
||||
activerecord (>= 7)
|
||||
swd (1.3.0)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.3)
|
||||
terminal-table (4.0.0)
|
||||
|
@ -859,9 +869,6 @@ GEM
|
|||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
|
@ -875,9 +882,10 @@ GEM
|
|||
openssl (>= 2.2)
|
||||
safety_net_attestation (~> 0.4.0)
|
||||
tpm-key_attestation (~> 0.14.0)
|
||||
webfinger (1.2.0)
|
||||
webfinger (2.1.3)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.25.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
|
@ -976,9 +984,9 @@ DEPENDENCIES
|
|||
omniauth-cas (~> 3.0.0.beta.1)
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
omniauth-saml (~> 2.0)
|
||||
omniauth_openid_connect (~> 0.6.1)
|
||||
opentelemetry-api (~> 1.4.0)
|
||||
opentelemetry-exporter-otlp (~> 0.29.0)
|
||||
omniauth_openid_connect (~> 0.8.0)
|
||||
opentelemetry-api (~> 1.5.0)
|
||||
opentelemetry-exporter-otlp (~> 0.30.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||
opentelemetry-instrumentation-active_model_serializers (~> 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
|
|
@ -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
|
|
@ -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
|
||||
width={50}
|
||||
height={28}
|
||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
||||
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
|
|
@ -153,6 +153,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
@ -12,12 +12,15 @@ import Overlay from 'react-overlays/Overlay';
|
|||
|
||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import emojiCompressed from 'flavours/glitch/features/emoji/emoji_compressed';
|
||||
import { useSystemEmojiFont } from 'flavours/glitch/initial_state';
|
||||
import { assetHost } from 'flavours/glitch/utils/config';
|
||||
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
|
||||
const nimblePickerData = emojiCompressed[5];
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||
|
@ -38,15 +41,18 @@ let EmojiPicker, Emoji; // load asynchronously
|
|||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||
|
||||
const notFoundFn = () => (
|
||||
<div className='emoji-mart-no-results'>
|
||||
<Emoji
|
||||
data={nimblePickerData}
|
||||
emoji='sleuth_or_spy'
|
||||
set='twitter'
|
||||
size={32}
|
||||
sheetSize={32}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
|
||||
|
@ -105,12 +111,12 @@ class ModifierPickerMenu extends PureComponent {
|
|||
|
||||
return (
|
||||
<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} native={useSystemEmojiFont} /></button>
|
||||
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></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} native={useSystemEmojiFont} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -145,7 +151,7 @@ class ModifierPicker extends PureComponent {
|
|||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers'>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||
<Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
</div>
|
||||
);
|
||||
|
@ -281,6 +287,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||
return (
|
||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||
<EmojiPicker
|
||||
data={nimblePickerData}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
|
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
|||
Category[],
|
||||
Data['aliases'],
|
||||
EmojisWithoutShortCodes,
|
||||
Data,
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
|
@ -9,18 +9,91 @@
|
|||
|
||||
// This version comment should be bumped each time the emoji data is changed
|
||||
// to ensure that the prevaled file is regenerated by Babel
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
const { emojiIndex } = require('emoji-mart');
|
||||
let data = require('emoji-mart/data/all.json');
|
||||
// This json file contains the names of the categories.
|
||||
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 _ = require('lodash');
|
||||
|
||||
|
||||
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 { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
|
||||
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||
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) {
|
||||
data = emojiMartUncompress(data);
|
||||
emojiMartUncompress(data);
|
||||
}
|
||||
|
||||
const emojiMartData = data;
|
||||
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
|||
const shortCodesToEmojiData = {};
|
||||
const emojisWithoutShortCodes = [];
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||
let emoji = emojiMart5Data.emojis[key];
|
||||
|
||||
// Emojis with skin tone modifiers are stored like this
|
||||
if (Object.hasOwn(emoji, '1')) {
|
||||
emoji = emoji['1'];
|
||||
}
|
||||
|
||||
shortcodeMap[emoji.native] = emoji.id;
|
||||
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||
});
|
||||
|
||||
const stripModifiers = unicode => {
|
||||
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
|||
}
|
||||
});
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMartData.emojis).forEach(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;
|
||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
|||
emojiMartData.categories,
|
||||
emojiMartData.aliases,
|
||||
emojisWithoutShortCodes,
|
||||
emojiMartData
|
||||
]));
|
||||
|
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||
|
||||
type Emojis = {
|
||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
||||
type Emojis = Record<
|
||||
NonNullable<keyof ShortCodesToEmojiData>,
|
||||
{
|
||||
native: BaseEmoji['native'];
|
||||
search: Search;
|
||||
short_names: Emoji['short_names'];
|
||||
unified: Emoji['unified'];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -9,12 +9,13 @@ import type {
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToFilename } from './unicode_to_filename';
|
||||
|
||||
type UnicodeMapping = {
|
||||
[key in FilenameData[number][0]]: {
|
||||
type UnicodeMapping = Record<
|
||||
FilenameData[number][0],
|
||||
{
|
||||
shortCode: ShortCodesToEmojiDataKey;
|
||||
filename: FilenameData[number][number];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -28,7 +28,7 @@ export const ColumnSettings: React.FC = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(key: string, checked: boolean) => {
|
||||
(key: string[], checked: boolean) => {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
[dispatch],
|
||||
|
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
|||
/>
|
||||
|
||||
<iframe
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
frameBorder='0'
|
||||
ref={iframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
|
|
|
@ -318,12 +318,26 @@ const expiresInFromExpiresAt = expires_at => {
|
|||
|
||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||
prefix = prefix.toLowerCase();
|
||||
|
||||
if (suggestions.length < 4) {
|
||||
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 })));
|
||||
} else {
|
||||
return suggestions;
|
||||
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
|
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
|||
<Sparklines
|
||||
width={50}
|
||||
height={28}
|
||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
||||
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
|
|
@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
@ -70,7 +70,7 @@ export const MediaItem: React.FC<{
|
|||
attachment.get('description')) as string | undefined;
|
||||
const previewUrl = attachment.get('preview_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 blurhash = attachment.get('blurhash') 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 { IconButton } from 'mastodon/components/icon_button';
|
||||
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
|
||||
const nimblePickerData = emojiCompressed[5];
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
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 backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||
|
||||
const notFoundFn = () => (
|
||||
<div className='emoji-mart-no-results'>
|
||||
<Emoji
|
||||
data={nimblePickerData}
|
||||
emoji='sleuth_or_spy'
|
||||
set='twitter'
|
||||
size={32}
|
||||
sheetSize={32}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
|
||||
|
@ -104,12 +110,12 @@ class ModifierPickerMenu extends PureComponent {
|
|||
|
||||
return (
|
||||
<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={2}><Emoji 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={4}><Emoji 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={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} 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 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 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 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 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 data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -144,7 +150,7 @@ class ModifierPicker extends PureComponent {
|
|||
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
|
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||
return (
|
||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||
<EmojiPicker
|
||||
data={nimblePickerData}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
|
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
|||
Category[],
|
||||
Data['aliases'],
|
||||
EmojisWithoutShortCodes,
|
||||
Data,
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
|
@ -9,18 +9,91 @@
|
|||
|
||||
// This version comment should be bumped each time the emoji data is changed
|
||||
// to ensure that the prevaled file is regenerated by Babel
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
const { emojiIndex } = require('emoji-mart');
|
||||
let data = require('emoji-mart/data/all.json');
|
||||
// This json file contains the names of the categories.
|
||||
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 _ = require('lodash');
|
||||
|
||||
|
||||
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 { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
|
||||
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||
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) {
|
||||
data = emojiMartUncompress(data);
|
||||
emojiMartUncompress(data);
|
||||
}
|
||||
|
||||
const emojiMartData = data;
|
||||
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
|||
const shortCodesToEmojiData = {};
|
||||
const emojisWithoutShortCodes = [];
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||
let emoji = emojiMart5Data.emojis[key];
|
||||
|
||||
// Emojis with skin tone modifiers are stored like this
|
||||
if (Object.hasOwn(emoji, '1')) {
|
||||
emoji = emoji['1'];
|
||||
}
|
||||
|
||||
shortcodeMap[emoji.native] = emoji.id;
|
||||
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||
});
|
||||
|
||||
const stripModifiers = unicode => {
|
||||
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
|||
}
|
||||
});
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMartData.emojis).forEach(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;
|
||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
|||
emojiMartData.categories,
|
||||
emojiMartData.aliases,
|
||||
emojisWithoutShortCodes,
|
||||
emojiMartData
|
||||
]));
|
||||
|
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||
|
||||
type Emojis = {
|
||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
||||
type Emojis = Record<
|
||||
NonNullable<keyof ShortCodesToEmojiData>,
|
||||
{
|
||||
native: BaseEmoji['native'];
|
||||
search: Search;
|
||||
short_names: Emoji['short_names'];
|
||||
unified: Emoji['unified'];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -9,12 +9,13 @@ import type {
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToFilename } from './unicode_to_filename';
|
||||
|
||||
type UnicodeMapping = {
|
||||
[key in FilenameData[number][0]]: {
|
||||
type UnicodeMapping = Record<
|
||||
FilenameData[number][0],
|
||||
{
|
||||
shortCode: ShortCodesToEmojiDataKey;
|
||||
filename: FilenameData[number][number];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(key: string, checked: boolean) => {
|
||||
(key: string[], checked: boolean) => {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
[dispatch],
|
||||
|
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
|||
/>
|
||||
|
||||
<iframe
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
frameBorder='0'
|
||||
ref={iframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
|
|
|
@ -242,12 +242,26 @@ const expiresInFromExpiresAt = expires_at => {
|
|||
|
||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||
prefix = prefix.toLowerCase();
|
||||
|
||||
if (suggestions.length < 4) {
|
||||
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 })));
|
||||
} else {
|
||||
return suggestions;
|
||||
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
|
|
|
@ -219,6 +219,15 @@ class UserMailer < Devise::Mailer
|
|||
end
|
||||
end
|
||||
|
||||
def announcement_published(user, announcement)
|
||||
@resource = user
|
||||
@announcement = announcement
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail subject: default_i18n_subject
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_devise_subject
|
||||
|
|
|
@ -5,16 +5,17 @@
|
|||
# Table name: announcements
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# text :text default(""), not null
|
||||
# published :boolean default(FALSE), not null
|
||||
# all_day :boolean default(FALSE), not null
|
||||
# ends_at :datetime
|
||||
# notification_sent_at :datetime
|
||||
# published :boolean default(FALSE), not null
|
||||
# published_at :datetime
|
||||
# scheduled_at :datetime
|
||||
# starts_at :datetime
|
||||
# ends_at :datetime
|
||||
# status_ids :bigint(8) is an Array
|
||||
# text :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# published_at :datetime
|
||||
# status_ids :bigint(8) is an Array
|
||||
#
|
||||
|
||||
class Announcement < ApplicationRecord
|
||||
|
@ -54,6 +55,10 @@ class Announcement < ApplicationRecord
|
|||
update!(published: false, scheduled_at: nil)
|
||||
end
|
||||
|
||||
def notification_sent?
|
||||
notification_sent_at.present?
|
||||
end
|
||||
|
||||
def mentions
|
||||
@mentions ||= Account.from_text(text)
|
||||
end
|
||||
|
@ -86,6 +91,10 @@ class Announcement < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def scope_for_notification
|
||||
User.confirmed.joins(:account).merge(Account.without_suspended)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def grouped_ordered_announcement_reactions
|
||||
|
|
|
@ -9,11 +9,12 @@ class TermsOfService::Generator
|
|||
admin_email
|
||||
arbitration_address
|
||||
arbitration_website
|
||||
choice_of_law
|
||||
dmca_address
|
||||
dmca_email
|
||||
domain
|
||||
jurisdiction
|
||||
choice_of_law
|
||||
min_age
|
||||
).freeze
|
||||
|
||||
attr_accessor(*VARIABLES)
|
||||
|
|
|
@ -16,4 +16,8 @@ class AnnouncementPolicy < ApplicationPolicy
|
|||
def destroy?
|
||||
role.can?(:manage_announcements)
|
||||
end
|
||||
|
||||
def distribute?
|
||||
record.published? && !record.notification_sent? && role.can?(:manage_settings)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
= l(announcement.created_at)
|
||||
|
||||
%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 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') }
|
||||
|
|
|
@ -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
|
||||
= form.input :domain, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :min_age, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :jurisdiction, wrapper: :with_label
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
# excluding the post deployment migrations.
|
||||
|
||||
unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
|
||||
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
|
||||
Mastodon::Database.add_post_migrate_path_to_rails
|
||||
|
|
|
@ -309,6 +309,7 @@ en:
|
|||
title: Audit log
|
||||
unavailable_instance: "(domain name unavailable)"
|
||||
announcements:
|
||||
back: Back to announcements
|
||||
destroyed_msg: Announcement successfully deleted!
|
||||
edit:
|
||||
title: Edit announcement
|
||||
|
@ -317,6 +318,9 @@ en:
|
|||
new:
|
||||
create: Create 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
|
||||
published_msg: Announcement successfully published!
|
||||
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.
|
||||
webauthn: Security keys
|
||||
user_mailer:
|
||||
announcement_published:
|
||||
description: 'The administrators of %{domain} are making an announcement:'
|
||||
subject: Service announcement
|
||||
title: "%{domain} service announcement"
|
||||
appeal_approved:
|
||||
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.
|
||||
|
|
|
@ -136,13 +136,14 @@ en:
|
|||
text: Can be structured with Markdown syntax.
|
||||
terms_of_service_generator:
|
||||
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_website: Can be a web form, 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.
|
||||
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_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.
|
||||
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:
|
||||
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
||||
role: The role controls which permissions the user has.
|
||||
|
@ -346,6 +347,7 @@ en:
|
|||
dmca_email: Email address for DMCA/copyright notices
|
||||
domain: Domain
|
||||
jurisdiction: Legal jurisdiction
|
||||
min_age: Minimum age
|
||||
user:
|
||||
role: Role
|
||||
time_zone: Time zone
|
||||
|
|
|
@ -50,6 +50,10 @@ namespace :admin do
|
|||
post :publish
|
||||
post :unpublish
|
||||
end
|
||||
|
||||
resource :preview, only: [:show], module: :announcements
|
||||
resource :test, only: [:create], module: :announcements
|
||||
resource :distribution, only: [:create], module: :announcements
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
@ -48,7 +48,7 @@ module.exports = merge(sharedConfig, {
|
|||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
||||
}),
|
||||
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 body = readFileSync(path);
|
||||
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 "published_at", precision: nil
|
||||
t.bigint "status_ids", array: true
|
||||
t.datetime "notification_sent_at"
|
||||
end
|
||||
|
||||
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 "updated_at", null: false
|
||||
t.date "effective_date"
|
||||
t.index ["effective_date"], name: "index_terms_of_services_on_effective_dat
|
||||
e", unique: true, where: "(effective_date IS NOT NULL)"
|
||||
t.index ["effective_date"], name: "index_terms_of_services_on_effective_date", unique: true, where: "(effective_date IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "tombstones", force: :cascade do |t|
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../mastodon/database'
|
||||
require_relative '../mastodon/snowflake'
|
||||
|
||||
module ActiveRecord
|
||||
|
@ -8,6 +9,8 @@ module ActiveRecord
|
|||
original_load_schema = instance_method(:load_schema)
|
||||
|
||||
define_method(:load_schema) do |db_config, *args|
|
||||
Mastodon::Database.add_post_migrate_path_to_rails(force: true)
|
||||
|
||||
ActiveRecord::Base.establish_connection(db_config)
|
||||
Mastodon::Snowflake.define_timestamp_id
|
||||
|
||||
|
|
|
@ -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'
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@mastodon/mastodon",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
@ -46,6 +46,7 @@
|
|||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@formatjs/intl-pluralrules": "^5.2.2",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
|
@ -183,10 +184,10 @@
|
|||
"eslint-define-config": "^2.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"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-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-hooks": "^5.0.0",
|
||||
"husky": "^9.0.11",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB |
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')))
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
module SystemHelpers
|
||||
def admin_user
|
||||
Fabricate(:admin_user)
|
||||
end
|
||||
|
||||
def submit_button
|
||||
I18n.t('generic.save_changes')
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
def text_label
|
||||
I18n.t('simple_form.labels.announcement.text')
|
||||
form_label('announcement.text')
|
||||
end
|
||||
|
||||
def admin_user
|
||||
|
|
|
@ -57,7 +57,7 @@ RSpec.describe 'Admin Invites' do
|
|||
end
|
||||
|
||||
def max_use_field
|
||||
I18n.t('simple_form.labels.defaults.max_uses')
|
||||
form_label('defaults.max_uses')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin Relationships' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
describe 'Viewing account relationships page' do
|
||||
|
|
|
@ -65,7 +65,7 @@ RSpec.describe 'Admin Rules' do
|
|||
end
|
||||
|
||||
def submit_form
|
||||
click_on I18n.t('generic.save_changes')
|
||||
click_on(submit_button)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin::Settings::About' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
it 'Saves changes to about settings' do
|
||||
sign_in admin_user
|
||||
visit admin_settings_about_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.settings.about.title'))
|
||||
|
||||
fill_in extended_description_field,
|
||||
with: 'new site description'
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin::Settings::Appearance' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
it 'Saves changes to appearance settings' do
|
||||
sign_in admin_user
|
||||
visit admin_settings_appearance_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.settings.appearance.title'))
|
||||
|
||||
fill_in custom_css_field,
|
||||
with: 'html { display: inline; }'
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin::Settings::Branding' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
it 'Saves changes to branding settings' do
|
||||
sign_in admin_user
|
||||
visit admin_settings_branding_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.settings.branding.title'))
|
||||
|
||||
fill_in short_description_field,
|
||||
with: 'new key value'
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
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
|
||||
sign_in admin_user
|
||||
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,
|
||||
with: '2'
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin::Settings::Discovery' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
it 'Saves changes to discovery settings' do
|
||||
sign_in admin_user
|
||||
visit admin_settings_discovery_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.settings.discovery.title'))
|
||||
|
||||
check trends_box
|
||||
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin::Settings::Registrations' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
it 'Saves changes to registrations settings' do
|
||||
sign_in admin_user
|
||||
visit admin_settings_registrations_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.settings.registrations.title'))
|
||||
|
||||
select open_mode_option,
|
||||
from: registrations_mode_field
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe 'Admin Tags' do
|
|||
end
|
||||
|
||||
def display_name_field
|
||||
I18n.t('simple_form.labels.defaults.display_name')
|
||||
form_label('defaults.display_name')
|
||||
end
|
||||
|
||||
def match_error_text
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin TermsOfService Drafts' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
describe 'Managing TOS drafts' do
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin TermsOfService Generates' do
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
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_jurisdiction', with: 'Europe'
|
||||
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 }
|
||||
.to change(TermsOfService, :count).by(1)
|
||||
expect(page)
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe 'Admin TermsOfService Previews' do
|
||||
let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
|
||||
let(:admin_user) { Fabricate(:admin_user) }
|
||||
|
||||
before { sign_in(admin_user) }
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ RSpec.describe 'Admin Warning Presets' do
|
|||
end
|
||||
|
||||
def submit_form
|
||||
click_on I18n.t('generic.save_changes')
|
||||
click_on(submit_button)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ RSpec.describe 'Admin Webhooks' do
|
|||
end
|
||||
|
||||
def submit_form
|
||||
click_on I18n.t('generic.save_changes')
|
||||
click_on(submit_button)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -108,6 +108,6 @@ RSpec.describe 'Filters' do
|
|||
end
|
||||
|
||||
def filter_title_field
|
||||
I18n.t('simple_form.labels.defaults.title')
|
||||
form_label('defaults.title')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,9 +71,9 @@ RSpec.describe 'Invites' do
|
|||
|
||||
def fill_invite_form
|
||||
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}"),
|
||||
from: I18n.t('simple_form.labels.defaults.expires_in')
|
||||
check I18n.t('simple_form.labels.defaults.autofollow')
|
||||
from: form_label('defaults.expires_in')
|
||||
check form_label('defaults.autofollow')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -96,7 +96,7 @@ RSpec.describe 'Settings applications page' do
|
|||
end
|
||||
|
||||
def submit_form
|
||||
click_on I18n.t('generic.save_changes')
|
||||
click_on(submit_button)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -33,18 +33,18 @@ RSpec.describe 'Settings preferences appearance page' do
|
|||
end
|
||||
|
||||
def confirm_delete_field
|
||||
I18n.t('simple_form.labels.defaults.setting_delete_modal')
|
||||
form_label('defaults.setting_delete_modal')
|
||||
end
|
||||
|
||||
def confirm_reblog_field
|
||||
I18n.t('simple_form.labels.defaults.setting_boost_modal')
|
||||
form_label('defaults.setting_boost_modal')
|
||||
end
|
||||
|
||||
def theme_selection_field
|
||||
I18n.t('simple_form.labels.defaults.setting_theme')
|
||||
form_label('defaults.setting_theme')
|
||||
end
|
||||
|
||||
def advanced_layout_field
|
||||
I18n.t('simple_form.labels.defaults.setting_advanced_layout')
|
||||
form_label('defaults.setting_advanced_layout')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,6 @@ RSpec.describe 'Settings preferences notifications page' do
|
|||
end
|
||||
|
||||
def notifications_follow_field
|
||||
I18n.t('simple_form.labels.notification_emails.follow')
|
||||
form_label('notification_emails.follow')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ RSpec.describe 'Settings preferences other page' do
|
|||
end
|
||||
|
||||
def mark_sensitive_field
|
||||
I18n.t('simple_form.labels.defaults.setting_default_sensitive')
|
||||
form_label('defaults.setting_default_sensitive')
|
||||
end
|
||||
|
||||
def language_field(key)
|
||||
|
|
|
@ -27,7 +27,7 @@ RSpec.describe 'Settings Privacy' do
|
|||
.to change { user.account.reload.discoverable }.to(true)
|
||||
expect(page)
|
||||
.to have_content(I18n.t('privacy.title'))
|
||||
.and have_content(I18n.t('generic.changes_saved_msg'))
|
||||
.and have_content(success_message)
|
||||
expect(ActivityPub::UpdateDistributionWorker)
|
||||
.to have_received(:perform_async).with(user.account.id)
|
||||
end
|
||||
|
|
|
@ -28,10 +28,10 @@ RSpec.describe 'Settings profile page' do
|
|||
end
|
||||
|
||||
def display_name_field
|
||||
I18n.t('simple_form.labels.defaults.display_name')
|
||||
form_label('defaults.display_name')
|
||||
end
|
||||
|
||||
def avatar_field
|
||||
I18n.t('simple_form.labels.defaults.avatar')
|
||||
form_label('defaults.avatar')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
def attribution_field
|
||||
I18n.t('simple_form.labels.account.attribution_domains')
|
||||
form_label('account.attribution_domains')
|
||||
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",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue