mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge pull request #3316 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 53be8392ec
This commit is contained in:
@@ -55,12 +55,31 @@ const preview: Preview = {
|
|||||||
locale: 'en',
|
locale: 'en',
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story, { parameters, globals, args }) => {
|
(Story, { parameters, globals, args, argTypes }) => {
|
||||||
// Get the locale from the global toolbar
|
// Get the locale from the global toolbar
|
||||||
// and merge it with any parameters or args state.
|
// and merge it with any parameters or args state.
|
||||||
const { locale } = globals as { locale: string };
|
const { locale } = globals as { locale: string };
|
||||||
const { state = {} } = parameters;
|
const { state = {} } = parameters;
|
||||||
const { state: argsState = {} } = args;
|
|
||||||
|
const argsState: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(args)) {
|
||||||
|
const argType = argTypes[key];
|
||||||
|
if (argType?.reduxPath) {
|
||||||
|
const reduxPath = Array.isArray(argType.reduxPath)
|
||||||
|
? argType.reduxPath.map((p) => p.toString())
|
||||||
|
: argType.reduxPath.split('.');
|
||||||
|
|
||||||
|
reduxPath.reduce((acc, key, i) => {
|
||||||
|
if (acc[key] === undefined) {
|
||||||
|
acc[key] = {};
|
||||||
|
}
|
||||||
|
if (i === reduxPath.length - 1) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc[key] as Record<string, unknown>;
|
||||||
|
}, argsState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reducer = reducerWithInitialState(
|
const reducer = reducerWithInitialState(
|
||||||
{
|
{
|
||||||
@@ -69,7 +88,7 @@ const preview: Preview = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
state as Record<string, unknown>,
|
state as Record<string, unknown>,
|
||||||
argsState as Record<string, unknown>,
|
argsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
// The addon package.json incorrectly exports types, so we need to override them here.
|
// The addon package.json incorrectly exports types, so we need to override them here.
|
||||||
|
|
||||||
|
import type { RootState } from '@/mastodon/store';
|
||||||
|
|
||||||
// See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76
|
// See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76
|
||||||
declare module '@storybook/addon-vitest/vitest-plugin' {
|
declare module '@storybook/addon-vitest/vitest-plugin' {
|
||||||
export * from '@storybook/addon-vitest/dist/vitest-plugin/index';
|
export * from '@storybook/addon-vitest/dist/vitest-plugin/index';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RootPathKeys = keyof RootState;
|
||||||
|
|
||||||
|
declare module 'storybook/internal/csf' {
|
||||||
|
export interface InputType {
|
||||||
|
reduxPath?:
|
||||||
|
| `${RootPathKeys}.${string}`
|
||||||
|
| [RootPathKeys, ...(string | number)[]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
2
Gemfile
2
Gemfile
@@ -138,7 +138,7 @@ group :test do
|
|||||||
# Browser integration testing
|
# Browser integration testing
|
||||||
gem 'capybara', '~> 3.39'
|
gem 'capybara', '~> 3.39'
|
||||||
gem 'capybara-playwright-driver'
|
gem 'capybara-playwright-driver'
|
||||||
gem 'playwright-ruby-client', '1.56.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
gem 'playwright-ruby-client', '1.57.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||||
|
|
||||||
# Used to reset the database between system tests
|
# Used to reset the database between system tests
|
||||||
gem 'database_cleaner-active_record'
|
gem 'database_cleaner-active_record'
|
||||||
|
|||||||
72
Gemfile.lock
72
Gemfile.lock
@@ -96,7 +96,7 @@ GEM
|
|||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1190.0)
|
aws-partitions (1.1194.0)
|
||||||
aws-sdk-core (3.239.2)
|
aws-sdk-core (3.239.2)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
@@ -108,7 +108,7 @@ GEM
|
|||||||
aws-sdk-kms (1.118.0)
|
aws-sdk-kms (1.118.0)
|
||||||
aws-sdk-core (~> 3, >= 3.239.1)
|
aws-sdk-core (~> 3, >= 3.239.1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.206.0)
|
aws-sdk-s3 (1.207.0)
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
@@ -166,7 +166,7 @@ GEM
|
|||||||
climate_control (1.2.0)
|
climate_control (1.2.0)
|
||||||
cocoon (1.2.15)
|
cocoon (1.2.15)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (2.5.5)
|
connection_pool (2.5.5)
|
||||||
cose (1.3.1)
|
cose (1.3.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
@@ -182,7 +182,7 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0)
|
database_cleaner-core (~> 2.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.5.0)
|
date (3.5.1)
|
||||||
debug (1.11.0)
|
debug (1.11.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
@@ -208,7 +208,7 @@ GEM
|
|||||||
domain_name (0.6.20240107)
|
domain_name (0.6.20240107)
|
||||||
doorkeeper (5.8.2)
|
doorkeeper (5.8.2)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (3.1.8)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
dry-cli (1.3.0)
|
dry-cli (1.3.0)
|
||||||
elasticsearch (7.17.11)
|
elasticsearch (7.17.11)
|
||||||
@@ -227,11 +227,11 @@ GEM
|
|||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erb (5.1.3)
|
erb (6.0.1)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.4.0)
|
et-orbi (1.4.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (1.3.0)
|
excon (1.3.2)
|
||||||
logger
|
logger
|
||||||
fabrication (3.0.0)
|
fabrication (3.0.0)
|
||||||
faker (3.5.3)
|
faker (3.5.3)
|
||||||
@@ -244,8 +244,8 @@ GEM
|
|||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (2.0.2)
|
faraday-httpclient (2.0.2)
|
||||||
httpclient (>= 2.2)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (3.4.1)
|
faraday-net_http (3.4.2)
|
||||||
net-http (>= 0.5.0)
|
net-http (~> 0.5)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.0)
|
||||||
ffi (1.17.2)
|
ffi (1.17.2)
|
||||||
@@ -269,20 +269,20 @@ GEM
|
|||||||
fog-openstack (1.1.5)
|
fog-openstack (1.1.5)
|
||||||
fog-core (~> 2.1)
|
fog-core (~> 2.1)
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
formatador (1.2.1)
|
formatador (1.2.3)
|
||||||
reline
|
reline
|
||||||
forwardable (1.3.3)
|
forwardable (1.3.3)
|
||||||
fugit (1.12.0)
|
fugit (1.12.1)
|
||||||
et-orbi (~> 1.4)
|
et-orbi (~> 1.4)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.3.0)
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (4.32.1)
|
google-protobuf (4.33.2)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
googleapis-common-protos-types (1.22.0)
|
googleapis-common-protos-types (1.22.0)
|
||||||
google-protobuf (~> 4.26)
|
google-protobuf (~> 4.26)
|
||||||
haml (6.4.0)
|
haml (7.1.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
@@ -291,7 +291,7 @@ GEM
|
|||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.67.0)
|
haml_lint (0.68.0)
|
||||||
haml (>= 5.0)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
@@ -340,7 +340,7 @@ GEM
|
|||||||
inline_svg (1.10.0)
|
inline_svg (1.10.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
io-console (0.8.1)
|
io-console (0.8.2)
|
||||||
irb (1.15.3)
|
irb (1.15.3)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
@@ -350,7 +350,7 @@ GEM
|
|||||||
azure-blob (~> 0.5.2)
|
azure-blob (~> 0.5.2)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.16.0)
|
json (2.18.0)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.17.0)
|
json-jwt (1.17.0)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
@@ -447,13 +447,13 @@ GEM
|
|||||||
mime-types-data (3.2025.0924)
|
mime-types-data (3.2025.0924)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.26.2)
|
minitest (5.27.0)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.17.0)
|
multi_json (1.18.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
net-http (0.6.0)
|
net-http (0.6.0)
|
||||||
uri
|
uri
|
||||||
net-imap (0.5.12)
|
net-imap (0.6.0)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.20.0)
|
net-ldap (0.20.0)
|
||||||
@@ -465,7 +465,7 @@ GEM
|
|||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.10)
|
nokogiri (1.18.10)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
@@ -594,7 +594,7 @@ GEM
|
|||||||
pg (1.6.2)
|
pg (1.6.2)
|
||||||
pghero (3.7.0)
|
pghero (3.7.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
playwright-ruby-client (1.56.0)
|
playwright-ruby-client (1.57.0)
|
||||||
concurrent-ruby (>= 1.1.6)
|
concurrent-ruby (>= 1.1.6)
|
||||||
mime-types (>= 3.0)
|
mime-types (>= 3.0)
|
||||||
pp (0.6.3)
|
pp (0.6.3)
|
||||||
@@ -615,7 +615,7 @@ GEM
|
|||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
psych (5.2.6)
|
psych (5.3.0)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (7.0.0)
|
public_suffix (7.0.0)
|
||||||
@@ -631,7 +631,7 @@ GEM
|
|||||||
rack-cors (3.0.0)
|
rack-cors (3.0.0)
|
||||||
logger
|
logger
|
||||||
rack (>= 3.0.14)
|
rack (>= 3.0.14)
|
||||||
rack-oauth2 (2.2.1)
|
rack-oauth2 (2.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
@@ -649,7 +649,7 @@ GEM
|
|||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.2.1)
|
rackup (2.3.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.0.3)
|
rails (8.0.3)
|
||||||
actioncable (= 8.0.3)
|
actioncable (= 8.0.3)
|
||||||
@@ -695,7 +695,7 @@ GEM
|
|||||||
readline (~> 0.0)
|
readline (~> 0.0)
|
||||||
rdf-normalize (0.7.0)
|
rdf-normalize (0.7.0)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rdoc (6.15.1)
|
rdoc (6.17.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
@@ -721,18 +721,18 @@ GEM
|
|||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 2.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (2.0.1)
|
rqrcode_core (2.0.1)
|
||||||
rspec (3.13.1)
|
rspec (3.13.2)
|
||||||
rspec-core (~> 3.13.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (~> 3.13.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
rspec-mocks (~> 3.13.0)
|
rspec-mocks (~> 3.13.0)
|
||||||
rspec-core (3.13.5)
|
rspec-core (3.13.6)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.5)
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-github (3.0.0)
|
rspec-github (3.0.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-mocks (3.13.5)
|
rspec-mocks (3.13.7)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (8.0.2)
|
rspec-rails (8.0.2)
|
||||||
@@ -820,7 +820,7 @@ GEM
|
|||||||
sidekiq-scheduler (6.0.1)
|
sidekiq-scheduler (6.0.1)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 7.3, < 9)
|
sidekiq (>= 7.3, < 9)
|
||||||
sidekiq-unique-jobs (8.0.11)
|
sidekiq-unique-jobs (8.0.12)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 7.0.0, < 9.0.0)
|
sidekiq (>= 7.0.0, < 9.0.0)
|
||||||
thor (>= 1.0, < 3.0)
|
thor (>= 1.0, < 3.0)
|
||||||
@@ -842,7 +842,7 @@ GEM
|
|||||||
stoplight (5.7.0)
|
stoplight (5.7.0)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
zeitwerk
|
zeitwerk
|
||||||
stringio (3.1.8)
|
stringio (3.1.9)
|
||||||
strong_migrations (2.5.1)
|
strong_migrations (2.5.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
swd (2.0.3)
|
swd (2.0.3)
|
||||||
@@ -859,7 +859,7 @@ GEM
|
|||||||
test-prof (1.5.0)
|
test-prof (1.5.0)
|
||||||
thor (1.4.0)
|
thor (1.4.0)
|
||||||
tilt (2.6.1)
|
tilt (2.6.1)
|
||||||
timeout (0.4.3)
|
timeout (0.5.0)
|
||||||
tpm-key_attestation (0.14.1)
|
tpm-key_attestation (0.14.1)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
@@ -880,7 +880,7 @@ GEM
|
|||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2025.2)
|
tzinfo-data (1.2025.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
@@ -920,7 +920,7 @@ GEM
|
|||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.9.1)
|
webrick (1.9.2)
|
||||||
websocket-driver (0.8.0)
|
websocket-driver (0.8.0)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
@@ -1033,7 +1033,7 @@ DEPENDENCIES
|
|||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
playwright-ruby-client (= 1.56.0)
|
playwright-ruby-client (= 1.57.0)
|
||||||
premailer-rails
|
premailer-rails
|
||||||
prometheus_exporter (~> 2.2)
|
prometheus_exporter (~> 2.2)
|
||||||
propshaft
|
propshaft
|
||||||
@@ -1090,7 +1090,7 @@ DEPENDENCIES
|
|||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.1p0
|
ruby 3.4.1p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.7.2
|
4.0.1
|
||||||
|
|||||||
@@ -57,27 +57,18 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
|||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_key
|
|
||||||
"wrapstodon:#{current_account.id}:#{year}"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def report_state
|
def report_state
|
||||||
return 'available' if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
AnnualReport.new(current_account, year).state do |async_refresh|
|
||||||
|
|
||||||
async_refresh = AsyncRefresh.new(refresh_key)
|
|
||||||
|
|
||||||
if async_refresh.running?
|
|
||||||
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||||
'generating'
|
|
||||||
elsif AnnualReport.current_campaign == year && AnnualReport.new(current_account, year).eligible?
|
|
||||||
'eligible'
|
|
||||||
else
|
|
||||||
'ineligible'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_key
|
||||||
|
"wrapstodon:#{current_account.id}:#{year}"
|
||||||
|
end
|
||||||
|
|
||||||
def year
|
def year
|
||||||
params[:id]&.to_i
|
params[:id]&.to_i
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module WrapstodonHelper
|
|||||||
).as_json
|
).as_json
|
||||||
|
|
||||||
payload[:me] = current_account.id.to_s if user_signed_in?
|
payload[:me] = current_account.id.to_s if user_signed_in?
|
||||||
|
payload[:domain] = Addressable::IDNA.to_unicode(Rails.configuration.x.local_domain)
|
||||||
|
|
||||||
json_string = payload.to_json
|
json_string = payload.to_json
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function loaded() {
|
|||||||
|
|
||||||
const initialState = JSON.parse(
|
const initialState = JSON.parse(
|
||||||
propsNode.textContent,
|
propsNode.textContent,
|
||||||
) as ApiAnnualReportResponse & { me?: string };
|
) as ApiAnnualReportResponse & { me?: string; domain: string };
|
||||||
|
|
||||||
const report = initialState.annual_reports[0];
|
const report = initialState.annual_reports[0];
|
||||||
if (!report) {
|
if (!report) {
|
||||||
@@ -38,6 +38,7 @@ function loaded() {
|
|||||||
meta: {
|
meta: {
|
||||||
locale: document.documentElement.lang,
|
locale: document.documentElement.lang,
|
||||||
me: initialState.me,
|
me: initialState.me,
|
||||||
|
domain: initialState.domain,
|
||||||
},
|
},
|
||||||
accounts: initialState.accounts,
|
accounts: initialState.accounts,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
@@ -31,9 +29,6 @@ export const fetchServer = () => (dispatch, getState) => {
|
|||||||
.get('/api/v2/instance').then(({ data }) => {
|
.get('/api/v2/instance').then(({ data }) => {
|
||||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||||
dispatch(fetchServerSuccess(data));
|
dispatch(fetchServerSuccess(data));
|
||||||
if (data.wrapstodon) {
|
|
||||||
void dispatch(checkAnnualReport());
|
|
||||||
}
|
|
||||||
}).catch(err => dispatch(fetchServerFail(err)));
|
}).catch(err => dispatch(fetchServerFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|||||||
|
|
||||||
import { Emoji } from './index';
|
import { Emoji } from './index';
|
||||||
|
|
||||||
type EmojiProps = ComponentProps<typeof Emoji> & { state: string };
|
type EmojiProps = ComponentProps<typeof Emoji> & {
|
||||||
|
style: 'auto' | 'native' | 'twemoji';
|
||||||
|
};
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Emoji',
|
title: 'Components/Emoji',
|
||||||
component: Emoji,
|
component: Emoji,
|
||||||
args: {
|
args: {
|
||||||
code: '🖤',
|
code: '🖤',
|
||||||
state: 'auto',
|
style: 'auto',
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
code: {
|
code: {
|
||||||
name: 'Emoji',
|
name: 'Emoji',
|
||||||
},
|
},
|
||||||
state: {
|
style: {
|
||||||
control: {
|
control: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -28,11 +30,7 @@ const meta = {
|
|||||||
},
|
},
|
||||||
options: ['auto', 'native', 'twemoji'],
|
options: ['auto', 'native', 'twemoji'],
|
||||||
name: 'Emoji Style',
|
name: 'Emoji Style',
|
||||||
mapping: {
|
reduxPath: 'meta.emoji_style',
|
||||||
auto: { meta: { emoji_style: 'auto' } },
|
|
||||||
native: { meta: { emoji_style: 'native' } },
|
|
||||||
twemoji: { meta: { emoji_style: 'twemoji' } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render(args) {
|
render(args) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function loaded() {
|
|||||||
|
|
||||||
const initialState = JSON.parse(
|
const initialState = JSON.parse(
|
||||||
propsNode.textContent,
|
propsNode.textContent,
|
||||||
) as ApiAnnualReportResponse & { me?: string };
|
) as ApiAnnualReportResponse & { me?: string; domain: string };
|
||||||
|
|
||||||
const report = initialState.annual_reports[0];
|
const report = initialState.annual_reports[0];
|
||||||
if (!report) {
|
if (!report) {
|
||||||
@@ -38,6 +38,7 @@ function loaded() {
|
|||||||
meta: {
|
meta: {
|
||||||
locale: document.documentElement.lang,
|
locale: document.documentElement.lang,
|
||||||
me: initialState.me,
|
me: initialState.me,
|
||||||
|
domain: initialState.domain,
|
||||||
},
|
},
|
||||||
accounts: initialState.accounts,
|
accounts: initialState.accounts,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import classNames from 'classnames/bind';
|
|||||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||||
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
||||||
|
import { getReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
createAppSelector,
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -26,7 +27,7 @@ import { NewPosts } from './new_posts';
|
|||||||
|
|
||||||
const moduleClassNames = classNames.bind(styles);
|
const moduleClassNames = classNames.bind(styles);
|
||||||
|
|
||||||
const accountSelector = createAppSelector(
|
export const accountSelector = createAppSelector(
|
||||||
[(state) => state.accounts, (state) => state.annualReport.report],
|
[(state) => state.accounts, (state) => state.annualReport.report],
|
||||||
(accounts, report) => {
|
(accounts, report) => {
|
||||||
if (report?.schema_version === 2) {
|
if (report?.schema_version === 2) {
|
||||||
@@ -43,6 +44,13 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const report = useAppSelector((state) => state.annualReport.report);
|
const report = useAppSelector((state) => state.annualReport.report);
|
||||||
const account = useAppSelector(accountSelector);
|
const account = useAppSelector(accountSelector);
|
||||||
|
const needsReport = !report; // Make into boolean to avoid object comparison in deps.
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (needsReport) {
|
||||||
|
void dispatch(getReport());
|
||||||
|
}
|
||||||
|
}, [dispatch, needsReport]);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||||
@@ -57,7 +65,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
|||||||
}
|
}
|
||||||
}, [pathname, initialPathname, close]);
|
}, [pathname, initialPathname, close]);
|
||||||
|
|
||||||
if (!report) {
|
if (needsReport) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||||
import {
|
import { generateReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
generateReport,
|
|
||||||
selectWrapstodonYear,
|
|
||||||
} from '@/flavours/glitch/reducers/slices/annual_report';
|
|
||||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
import { AnnualReport } from '.';
|
import { AnnualReport } from '.';
|
||||||
@@ -21,8 +18,7 @@ const AnnualReportModal: React.FC<{
|
|||||||
onChangeBackgroundColor('var(--color-bg-media-base)');
|
onChangeBackgroundColor('var(--color-bg-media-base)');
|
||||||
}, [onChangeBackgroundColor]);
|
}, [onChangeBackgroundColor]);
|
||||||
|
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
|
|
||||||
const showAnnouncement = year && state && state !== 'available';
|
const showAnnouncement = year && state && state !== 'available';
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import { openModal } from '@/flavours/glitch/actions/modal';
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { Icon } from '@/flavours/glitch/components/icon';
|
import { Icon } from '@/flavours/glitch/components/icon';
|
||||||
import { selectWrapstodonYear } from '@/flavours/glitch/reducers/slices/annual_report';
|
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
createAppSelector,
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -23,8 +22,7 @@ const selectReportModalOpen = createAppSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AnnualReportNavItem: FC = () => {
|
export const AnnualReportNavItem: FC = () => {
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
const active = useAppSelector(selectReportModalOpen);
|
const active = useAppSelector(selectReportModalOpen);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -16,22 +16,16 @@ $mobile-breakpoint: 540px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 1.8rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
strong {
|
||||||
width: 2rem;
|
font-weight: 600;
|
||||||
opacity: 0.6;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
a:any-link {
|
a:any-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -43,3 +37,22 @@ $mobile-breakpoint: 540px;
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkList {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,43 +2,66 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { DisplayName } from '@/flavours/glitch/components/display_name';
|
||||||
import { IconLogo } from '@/flavours/glitch/components/logo';
|
import { IconLogo } from '@/flavours/glitch/components/logo';
|
||||||
import { useAppSelector } from '@/flavours/glitch/store';
|
import { useAppSelector } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
import { AnnualReport } from './index';
|
import { AnnualReport, accountSelector } from './index';
|
||||||
import classes from './shared_page.module.scss';
|
import classes from './shared_page.module.scss';
|
||||||
|
|
||||||
export const WrapstodonSharedPage: FC = () => {
|
export const WrapstodonSharedPage: FC = () => {
|
||||||
const isLoggedIn = useAppSelector((state) => !!state.meta.get('me'));
|
const account = useAppSelector(accountSelector);
|
||||||
|
const domain = useAppSelector((state) => state.meta.get('domain') as string);
|
||||||
return (
|
return (
|
||||||
<main className={classes.wrapper}>
|
<main className={classes.wrapper}>
|
||||||
<AnnualReport />
|
<AnnualReport />
|
||||||
<footer className={classes.footer}>
|
<footer className={classes.footer}>
|
||||||
<IconLogo className={classes.logo} />
|
<div className={classes.footerSection}>
|
||||||
<FormattedMessage
|
<IconLogo className={classes.logo} />
|
||||||
id='annual_report.shared_page.footer'
|
<FormattedMessage
|
||||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
id='annual_report.shared_page.footer'
|
||||||
values={{ heart: '🐘' }}
|
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||||
/>
|
values={{ heart: '🐘' }}
|
||||||
<nav className={classes.nav}>
|
tagName='p'
|
||||||
<a href='https://joinmastodon.org'>
|
/>
|
||||||
<FormattedMessage id='footer.about' defaultMessage='About' />
|
<ul className={classes.linkList}>
|
||||||
</a>
|
<li>
|
||||||
{!isLoggedIn && (
|
<a href='https://joinmastodon.org'>
|
||||||
<a href='https://joinmastodon.org/servers'>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='footer.about_mastodon'
|
||||||
id='annual_report.shared_page.sign_up'
|
defaultMessage='About Mastodon'
|
||||||
defaultMessage='Sign up'
|
/>
|
||||||
/>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
)}
|
<li>
|
||||||
<a href='https://joinmastodon.org/sponsors'>
|
<a href='https://joinmastodon.org/sponsors'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.shared_page.donate'
|
||||||
|
defaultMessage='Donate'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.footerSection}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.shared_page.footer_server_info'
|
||||||
|
defaultMessage='{username} uses {domain}, one of many communities powered by Mastodon.'
|
||||||
|
values={{
|
||||||
|
username: <DisplayName variant='simple' account={account} />,
|
||||||
|
domain: <strong>{domain}</strong>,
|
||||||
|
}}
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
<a href='/about'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.shared_page.donate'
|
id='footer.about_server'
|
||||||
defaultMessage='Donate'
|
defaultMessage='About {domain}'
|
||||||
|
values={{ domain }}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import { openModal } from '@/flavours/glitch/actions/modal';
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { useDismissible } from '@/flavours/glitch/hooks/useDismissible';
|
import { useDismissible } from '@/flavours/glitch/hooks/useDismissible';
|
||||||
import {
|
import { generateReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
generateReport,
|
|
||||||
selectWrapstodonYear,
|
|
||||||
} from '@/flavours/glitch/reducers/slices/annual_report';
|
|
||||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
import { AnnualReportAnnouncement } from './announcement';
|
import { AnnualReportAnnouncement } from './announcement';
|
||||||
|
|
||||||
export const AnnualReportTimeline: FC = () => {
|
export const AnnualReportTimeline: FC = () => {
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleBuildRequest = useCallback(() => {
|
const handleBuildRequest = useCallback(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { List } from 'immutable';
|
|||||||
|
|
||||||
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
||||||
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
||||||
|
import type { CustomEmoji } from '@/flavours/glitch/models/custom_emoji';
|
||||||
import type { Status } from '@/flavours/glitch/models/status';
|
import type { Status } from '@/flavours/glitch/models/status';
|
||||||
|
|
||||||
import type { Mention } from './embedded_status';
|
import type { Mention } from './embedded_status';
|
||||||
@@ -33,6 +34,7 @@ export const EmbeddedStatusContent: React.FC<{
|
|||||||
className={className}
|
className={className}
|
||||||
lang={status.get('language') as string}
|
lang={status.get('language') as string}
|
||||||
htmlString={status.get('contentHtml') as string}
|
htmlString={status.get('contentHtml') as string}
|
||||||
|
extraEmojis={status.get('emoji') as List<CustomEmoji>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity
|
|||||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||||
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
@@ -411,6 +412,7 @@ class UI extends PureComponent {
|
|||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(fetchNotifications());
|
this.props.dispatch(fetchNotifications());
|
||||||
this.props.dispatch(fetchServerTranslationLanguages());
|
this.props.dispatch(fetchServerTranslationLanguages());
|
||||||
|
this.props.dispatch(checkAnnualReport());
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ApiAnnualReportState } from './api/annual_report';
|
||||||
import type { ApiAccountJSON } from './api_types/accounts';
|
import type { ApiAccountJSON } from './api_types/accounts';
|
||||||
|
|
||||||
type InitialStateLanguage = [code: string, name: string, localName: string];
|
type InitialStateLanguage = [code: string, name: string, localName: string];
|
||||||
@@ -49,6 +50,7 @@ interface InitialStateMeta {
|
|||||||
status_page_url: string;
|
status_page_url: string;
|
||||||
terms_of_service_enabled: boolean;
|
terms_of_service_enabled: boolean;
|
||||||
emoji_style?: string;
|
emoji_style?: string;
|
||||||
|
wrapstodon?: InitialWrapstodonState | null;
|
||||||
default_content_type: string;
|
default_content_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +69,11 @@ interface PollLimits {
|
|||||||
max_expiration: number;
|
max_expiration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InitialWrapstodonState {
|
||||||
|
year: number;
|
||||||
|
state: ApiAnnualReportState;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InitialState {
|
export interface InitialState {
|
||||||
accounts: Record<string, ApiAccountJSON>;
|
accounts: Record<string, ApiAccountJSON>;
|
||||||
languages: InitialStateLanguage[];
|
languages: InitialStateLanguage[];
|
||||||
@@ -155,6 +162,7 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
|||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const sso_redirect = getMeta('sso_redirect');
|
export const sso_redirect = getMeta('sso_redirect');
|
||||||
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
|
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
|
||||||
|
export const wrapstodon = getMeta('wrapstodon');
|
||||||
|
|
||||||
const displayNames =
|
const displayNames =
|
||||||
// Intl.DisplayNames can be undefined in old browsers
|
// Intl.DisplayNames can be undefined in old browsers
|
||||||
|
|||||||
@@ -11,22 +11,25 @@ import {
|
|||||||
apiGetAnnualReportState,
|
apiGetAnnualReportState,
|
||||||
apiRequestGenerateAnnualReport,
|
apiRequestGenerateAnnualReport,
|
||||||
} from '@/flavours/glitch/api/annual_report';
|
} from '@/flavours/glitch/api/annual_report';
|
||||||
|
import { wrapstodon } from '@/flavours/glitch/initial_state';
|
||||||
import type { AnnualReport } from '@/flavours/glitch/models/annual_report';
|
import type { AnnualReport } from '@/flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
|
||||||
createAppThunk,
|
createAppThunk,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
} from '../../store/typed_functions';
|
} from '@/flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
interface AnnualReportState {
|
interface AnnualReportState {
|
||||||
|
year?: number;
|
||||||
state?: ApiAnnualReportState;
|
state?: ApiAnnualReportState;
|
||||||
report?: AnnualReport;
|
report?: AnnualReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
const annualReportSlice = createSlice({
|
const annualReportSlice = createSlice({
|
||||||
name: 'annualReport',
|
name: 'annualReport',
|
||||||
initialState: {} as AnnualReportState,
|
initialState: {
|
||||||
|
year: wrapstodon?.year,
|
||||||
|
state: wrapstodon?.state,
|
||||||
|
} as AnnualReportState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setReport(state, action: PayloadAction<AnnualReport>) {
|
setReport(state, action: PayloadAction<AnnualReport>) {
|
||||||
state.report = action.payload;
|
state.report = action.payload;
|
||||||
@@ -52,18 +55,17 @@ const annualReportSlice = createSlice({
|
|||||||
export const annualReport = annualReportSlice.reducer;
|
export const annualReport = annualReportSlice.reducer;
|
||||||
export const { setReport } = annualReportSlice.actions;
|
export const { setReport } = annualReportSlice.actions;
|
||||||
|
|
||||||
export const selectWrapstodonYear = createAppSelector(
|
// Called on initial load to check if we need to refresh the report state.
|
||||||
[(state) => state.server.getIn(['server', 'wrapstodon'])],
|
|
||||||
(year: unknown) => (typeof year === 'number' && year > 2000 ? year : null),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This kicks everything off, and is called after fetching the server info.
|
|
||||||
export const checkAnnualReport = createAppThunk(
|
export const checkAnnualReport = createAppThunk(
|
||||||
`${annualReportSlice.name}/checkAnnualReport`,
|
`${annualReportSlice.name}/checkAnnualReport`,
|
||||||
(_arg: unknown, { dispatch, getState }) => {
|
(_arg: unknown, { dispatch, getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { state, year } = getState().annualReport;
|
||||||
const me = getState().meta.get('me') as string;
|
const me = getState().meta.get('me') as string;
|
||||||
if (!year || !me) {
|
|
||||||
|
// If we have a state, we only need to fetch it again to poll for changes.
|
||||||
|
const needsStateRefresh = !state || state === 'generating';
|
||||||
|
|
||||||
|
if (!year || !me || !needsStateRefresh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void dispatch(fetchReportState());
|
void dispatch(fetchReportState());
|
||||||
@@ -73,7 +75,7 @@ export const checkAnnualReport = createAppThunk(
|
|||||||
const fetchReportState = createDataLoadingThunk(
|
const fetchReportState = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/fetchReportState`,
|
`${annualReportSlice.name}/fetchReportState`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
@@ -84,8 +86,6 @@ const fetchReportState = createDataLoadingThunk(
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
void dispatch(fetchReportState());
|
void dispatch(fetchReportState());
|
||||||
}, 1_000 * refresh.retry);
|
}, 1_000 * refresh.retry);
|
||||||
} else if (state === 'available') {
|
|
||||||
void dispatch(getReport());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
@@ -97,7 +97,7 @@ const fetchReportState = createDataLoadingThunk(
|
|||||||
export const generateReport = createDataLoadingThunk(
|
export const generateReport = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/generateReport`,
|
`${annualReportSlice.name}/generateReport`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export const generateReport = createDataLoadingThunk(
|
|||||||
export const getReport = createDataLoadingThunk(
|
export const getReport = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/getReport`,
|
`${annualReportSlice.name}/getReport`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2913,7 +2913,7 @@ a.account__display-name {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: rgb(from var(--color-text-disabled) r g b / 70%);
|
color: var(--color-text-on-disabled);
|
||||||
background: var(--color-bg-disabled);
|
background: var(--color-bg-disabled);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@@ -4059,8 +4059,8 @@ a.account__display-name {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:active,
|
||||||
&:active {
|
&:focus-visible {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4078,14 +4078,7 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--logo {
|
&--logo {
|
||||||
background: transparent;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
@@ -31,9 +29,6 @@ export const fetchServer = () => (dispatch, getState) => {
|
|||||||
.get('/api/v2/instance').then(({ data }) => {
|
.get('/api/v2/instance').then(({ data }) => {
|
||||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||||
dispatch(fetchServerSuccess(data));
|
dispatch(fetchServerSuccess(data));
|
||||||
if (data.wrapstodon) {
|
|
||||||
void dispatch(checkAnnualReport());
|
|
||||||
}
|
|
||||||
}).catch(err => dispatch(fetchServerFail(err)));
|
}).catch(err => dispatch(fetchServerFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||||
|
|
||||||
import { Account } from './index';
|
import { Account } from './index';
|
||||||
|
|
||||||
|
type Props = Omit<ComponentProps<typeof Account>, 'id'> & {
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Account',
|
title: 'Components/Account',
|
||||||
component: Account,
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
id: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'ID of the account to display',
|
description: 'The display name of the account',
|
||||||
|
reduxPath: 'accounts.1.display_name_html',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The username of the account',
|
||||||
|
reduxPath: 'accounts.1.acct',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -40,7 +52,8 @@ const meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
id: '1',
|
name: 'Test User',
|
||||||
|
username: 'testuser',
|
||||||
size: 46,
|
size: 46,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
minimal: false,
|
minimal: false,
|
||||||
@@ -55,17 +68,16 @@ const meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof Account>;
|
render(args) {
|
||||||
|
return <Account id='1' {...args} />;
|
||||||
|
},
|
||||||
|
} satisfies Meta<Props>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {};
|
||||||
args: {
|
|
||||||
id: '1',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Hidden: Story = {
|
export const Hidden: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|||||||
|
|
||||||
import { Emoji } from './index';
|
import { Emoji } from './index';
|
||||||
|
|
||||||
type EmojiProps = ComponentProps<typeof Emoji> & { state: string };
|
type EmojiProps = ComponentProps<typeof Emoji> & {
|
||||||
|
style: 'auto' | 'native' | 'twemoji';
|
||||||
|
};
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Emoji',
|
title: 'Components/Emoji',
|
||||||
component: Emoji,
|
component: Emoji,
|
||||||
args: {
|
args: {
|
||||||
code: '🖤',
|
code: '🖤',
|
||||||
state: 'auto',
|
style: 'auto',
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
code: {
|
code: {
|
||||||
name: 'Emoji',
|
name: 'Emoji',
|
||||||
},
|
},
|
||||||
state: {
|
style: {
|
||||||
control: {
|
control: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -28,11 +30,7 @@ const meta = {
|
|||||||
},
|
},
|
||||||
options: ['auto', 'native', 'twemoji'],
|
options: ['auto', 'native', 'twemoji'],
|
||||||
name: 'Emoji Style',
|
name: 'Emoji Style',
|
||||||
mapping: {
|
reduxPath: 'meta.emoji_style',
|
||||||
auto: { meta: { emoji_style: 'auto' } },
|
|
||||||
native: { meta: { emoji_style: 'native' } },
|
|
||||||
twemoji: { meta: { emoji_style: 'twemoji' } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render(args) {
|
render(args) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import classNames from 'classnames/bind';
|
|||||||
import { closeModal } from '@/mastodon/actions/modal';
|
import { closeModal } from '@/mastodon/actions/modal';
|
||||||
import { IconButton } from '@/mastodon/components/icon_button';
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||||
|
import { getReport } from '@/mastodon/reducers/slices/annual_report';
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
createAppSelector,
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -26,7 +27,7 @@ import { NewPosts } from './new_posts';
|
|||||||
|
|
||||||
const moduleClassNames = classNames.bind(styles);
|
const moduleClassNames = classNames.bind(styles);
|
||||||
|
|
||||||
const accountSelector = createAppSelector(
|
export const accountSelector = createAppSelector(
|
||||||
[(state) => state.accounts, (state) => state.annualReport.report],
|
[(state) => state.accounts, (state) => state.annualReport.report],
|
||||||
(accounts, report) => {
|
(accounts, report) => {
|
||||||
if (report?.schema_version === 2) {
|
if (report?.schema_version === 2) {
|
||||||
@@ -43,6 +44,13 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const report = useAppSelector((state) => state.annualReport.report);
|
const report = useAppSelector((state) => state.annualReport.report);
|
||||||
const account = useAppSelector(accountSelector);
|
const account = useAppSelector(accountSelector);
|
||||||
|
const needsReport = !report; // Make into boolean to avoid object comparison in deps.
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (needsReport) {
|
||||||
|
void dispatch(getReport());
|
||||||
|
}
|
||||||
|
}, [dispatch, needsReport]);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||||
@@ -57,7 +65,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
|||||||
}
|
}
|
||||||
}, [pathname, initialPathname, close]);
|
}, [pathname, initialPathname, close]);
|
||||||
|
|
||||||
if (!report) {
|
if (needsReport) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { closeModal } from '@/mastodon/actions/modal';
|
import { closeModal } from '@/mastodon/actions/modal';
|
||||||
import {
|
import { generateReport } from '@/mastodon/reducers/slices/annual_report';
|
||||||
generateReport,
|
|
||||||
selectWrapstodonYear,
|
|
||||||
} from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { AnnualReport } from '.';
|
import { AnnualReport } from '.';
|
||||||
@@ -21,8 +18,7 @@ const AnnualReportModal: React.FC<{
|
|||||||
onChangeBackgroundColor('var(--color-bg-media-base)');
|
onChangeBackgroundColor('var(--color-bg-media-base)');
|
||||||
}, [onChangeBackgroundColor]);
|
}, [onChangeBackgroundColor]);
|
||||||
|
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
|
|
||||||
const showAnnouncement = year && state && state !== 'available';
|
const showAnnouncement = year && state && state !== 'available';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import classNames from 'classnames';
|
|||||||
import IconPlanet from '@/images/icons/icon_planet.svg?react';
|
import IconPlanet from '@/images/icons/icon_planet.svg?react';
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
import { selectWrapstodonYear } from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
createAppSelector,
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -23,8 +22,7 @@ const selectReportModalOpen = createAppSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AnnualReportNavItem: FC = () => {
|
export const AnnualReportNavItem: FC = () => {
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
const active = useAppSelector(selectReportModalOpen);
|
const active = useAppSelector(selectReportModalOpen);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -16,22 +16,16 @@ $mobile-breakpoint: 540px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 1.8rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
strong {
|
||||||
width: 2rem;
|
font-weight: 600;
|
||||||
opacity: 0.6;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
a:any-link {
|
a:any-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -43,3 +37,22 @@ $mobile-breakpoint: 540px;
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkList {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,43 +2,66 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { DisplayName } from '@/mastodon/components/display_name';
|
||||||
import { IconLogo } from '@/mastodon/components/logo';
|
import { IconLogo } from '@/mastodon/components/logo';
|
||||||
import { useAppSelector } from '@/mastodon/store';
|
import { useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { AnnualReport } from './index';
|
import { AnnualReport, accountSelector } from './index';
|
||||||
import classes from './shared_page.module.scss';
|
import classes from './shared_page.module.scss';
|
||||||
|
|
||||||
export const WrapstodonSharedPage: FC = () => {
|
export const WrapstodonSharedPage: FC = () => {
|
||||||
const isLoggedIn = useAppSelector((state) => !!state.meta.get('me'));
|
const account = useAppSelector(accountSelector);
|
||||||
|
const domain = useAppSelector((state) => state.meta.get('domain') as string);
|
||||||
return (
|
return (
|
||||||
<main className={classes.wrapper}>
|
<main className={classes.wrapper}>
|
||||||
<AnnualReport />
|
<AnnualReport />
|
||||||
<footer className={classes.footer}>
|
<footer className={classes.footer}>
|
||||||
<IconLogo className={classes.logo} />
|
<div className={classes.footerSection}>
|
||||||
<FormattedMessage
|
<IconLogo className={classes.logo} />
|
||||||
id='annual_report.shared_page.footer'
|
<FormattedMessage
|
||||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
id='annual_report.shared_page.footer'
|
||||||
values={{ heart: '🐘' }}
|
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||||
/>
|
values={{ heart: '🐘' }}
|
||||||
<nav className={classes.nav}>
|
tagName='p'
|
||||||
<a href='https://joinmastodon.org'>
|
/>
|
||||||
<FormattedMessage id='footer.about' defaultMessage='About' />
|
<ul className={classes.linkList}>
|
||||||
</a>
|
<li>
|
||||||
{!isLoggedIn && (
|
<a href='https://joinmastodon.org'>
|
||||||
<a href='https://joinmastodon.org/servers'>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='footer.about_mastodon'
|
||||||
id='annual_report.shared_page.sign_up'
|
defaultMessage='About Mastodon'
|
||||||
defaultMessage='Sign up'
|
/>
|
||||||
/>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
)}
|
<li>
|
||||||
<a href='https://joinmastodon.org/sponsors'>
|
<a href='https://joinmastodon.org/sponsors'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.shared_page.donate'
|
||||||
|
defaultMessage='Donate'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.footerSection}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.shared_page.footer_server_info'
|
||||||
|
defaultMessage='{username} uses {domain}, one of many communities powered by Mastodon.'
|
||||||
|
values={{
|
||||||
|
username: <DisplayName variant='simple' account={account} />,
|
||||||
|
domain: <strong>{domain}</strong>,
|
||||||
|
}}
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
<a href='/about'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.shared_page.donate'
|
id='footer.about_server'
|
||||||
defaultMessage='Donate'
|
defaultMessage='About {domain}'
|
||||||
|
values={{ domain }}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||||
import {
|
import { generateReport } from '@/mastodon/reducers/slices/annual_report';
|
||||||
generateReport,
|
|
||||||
selectWrapstodonYear,
|
|
||||||
} from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { AnnualReportAnnouncement } from './announcement';
|
import { AnnualReportAnnouncement } from './announcement';
|
||||||
|
|
||||||
export const AnnualReportTimeline: FC = () => {
|
export const AnnualReportTimeline: FC = () => {
|
||||||
const { state } = useAppSelector((state) => state.annualReport);
|
const { state, year } = useAppSelector((state) => state.annualReport);
|
||||||
const year = useAppSelector(selectWrapstodonYear);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleBuildRequest = useCallback(() => {
|
const handleBuildRequest = useCallback(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { List } from 'immutable';
|
|||||||
|
|
||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||||
|
import type { CustomEmoji } from '@/mastodon/models/custom_emoji';
|
||||||
import type { Status } from '@/mastodon/models/status';
|
import type { Status } from '@/mastodon/models/status';
|
||||||
|
|
||||||
import type { Mention } from './embedded_status';
|
import type { Mention } from './embedded_status';
|
||||||
@@ -33,6 +34,7 @@ export const EmbeddedStatusContent: React.FC<{
|
|||||||
className={className}
|
className={className}
|
||||||
lang={status.get('language') as string}
|
lang={status.get('language') as string}
|
||||||
htmlString={status.get('contentHtml') as string}
|
htmlString={status.get('contentHtml') as string}
|
||||||
|
extraEmojis={status.get('emoji') as List<CustomEmoji>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { PictureInPicture } from 'mastodon/features/picture_in_picture';
|
|||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
|
||||||
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
@@ -396,6 +397,7 @@ class UI extends PureComponent {
|
|||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(fetchNotifications());
|
this.props.dispatch(fetchNotifications());
|
||||||
this.props.dispatch(fetchServerTranslationLanguages());
|
this.props.dispatch(fetchServerTranslationLanguages());
|
||||||
|
this.props.dispatch(checkAnnualReport());
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ApiAnnualReportState } from './api/annual_report';
|
||||||
import type { ApiAccountJSON } from './api_types/accounts';
|
import type { ApiAccountJSON } from './api_types/accounts';
|
||||||
|
|
||||||
type InitialStateLanguage = [code: string, name: string, localName: string];
|
type InitialStateLanguage = [code: string, name: string, localName: string];
|
||||||
@@ -47,6 +48,7 @@ interface InitialStateMeta {
|
|||||||
status_page_url: string;
|
status_page_url: string;
|
||||||
terms_of_service_enabled: boolean;
|
terms_of_service_enabled: boolean;
|
||||||
emoji_style?: string;
|
emoji_style?: string;
|
||||||
|
wrapstodon?: InitialWrapstodonState | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Role {
|
interface Role {
|
||||||
@@ -57,6 +59,11 @@ interface Role {
|
|||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InitialWrapstodonState {
|
||||||
|
year: number;
|
||||||
|
state: ApiAnnualReportState;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InitialState {
|
export interface InitialState {
|
||||||
accounts: Record<string, ApiAccountJSON>;
|
accounts: Record<string, ApiAccountJSON>;
|
||||||
languages: InitialStateLanguage[];
|
languages: InitialStateLanguage[];
|
||||||
@@ -128,6 +135,7 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
|||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const sso_redirect = getMeta('sso_redirect');
|
export const sso_redirect = getMeta('sso_redirect');
|
||||||
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
|
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
|
||||||
|
export const wrapstodon = getMeta('wrapstodon');
|
||||||
|
|
||||||
const displayNames =
|
const displayNames =
|
||||||
// Intl.DisplayNames can be undefined in old browsers
|
// Intl.DisplayNames can be undefined in old browsers
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"annual_report.nav_item.badge": "New",
|
"annual_report.nav_item.badge": "New",
|
||||||
"annual_report.shared_page.donate": "Donate",
|
"annual_report.shared_page.donate": "Donate",
|
||||||
"annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team",
|
"annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team",
|
||||||
"annual_report.shared_page.sign_up": "Sign up",
|
"annual_report.shared_page.footer_server_info": "{username} uses {domain}, one of many communities powered by Mastodon.",
|
||||||
"annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
"annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||||
"annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
"annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||||
"annual_report.summary.archetype.booster.name": "The Archer",
|
"annual_report.summary.archetype.booster.name": "The Archer",
|
||||||
@@ -441,6 +441,8 @@
|
|||||||
"follow_suggestions.who_to_follow": "Who to follow",
|
"follow_suggestions.who_to_follow": "Who to follow",
|
||||||
"followed_tags": "Followed hashtags",
|
"followed_tags": "Followed hashtags",
|
||||||
"footer.about": "About",
|
"footer.about": "About",
|
||||||
|
"footer.about_mastodon": "About Mastodon",
|
||||||
|
"footer.about_server": "About {domain}",
|
||||||
"footer.about_this_server": "About",
|
"footer.about_this_server": "About",
|
||||||
"footer.directory": "Profiles directory",
|
"footer.directory": "Profiles directory",
|
||||||
"footer.get_app": "Get the app",
|
"footer.get_app": "Get the app",
|
||||||
|
|||||||
@@ -11,22 +11,25 @@ import {
|
|||||||
apiGetAnnualReportState,
|
apiGetAnnualReportState,
|
||||||
apiRequestGenerateAnnualReport,
|
apiRequestGenerateAnnualReport,
|
||||||
} from '@/mastodon/api/annual_report';
|
} from '@/mastodon/api/annual_report';
|
||||||
|
import { wrapstodon } from '@/mastodon/initial_state';
|
||||||
import type { AnnualReport } from '@/mastodon/models/annual_report';
|
import type { AnnualReport } from '@/mastodon/models/annual_report';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createAppSelector,
|
|
||||||
createAppThunk,
|
createAppThunk,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
} from '../../store/typed_functions';
|
} from '@/mastodon/store/typed_functions';
|
||||||
|
|
||||||
interface AnnualReportState {
|
interface AnnualReportState {
|
||||||
|
year?: number;
|
||||||
state?: ApiAnnualReportState;
|
state?: ApiAnnualReportState;
|
||||||
report?: AnnualReport;
|
report?: AnnualReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
const annualReportSlice = createSlice({
|
const annualReportSlice = createSlice({
|
||||||
name: 'annualReport',
|
name: 'annualReport',
|
||||||
initialState: {} as AnnualReportState,
|
initialState: {
|
||||||
|
year: wrapstodon?.year,
|
||||||
|
state: wrapstodon?.state,
|
||||||
|
} as AnnualReportState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setReport(state, action: PayloadAction<AnnualReport>) {
|
setReport(state, action: PayloadAction<AnnualReport>) {
|
||||||
state.report = action.payload;
|
state.report = action.payload;
|
||||||
@@ -52,18 +55,17 @@ const annualReportSlice = createSlice({
|
|||||||
export const annualReport = annualReportSlice.reducer;
|
export const annualReport = annualReportSlice.reducer;
|
||||||
export const { setReport } = annualReportSlice.actions;
|
export const { setReport } = annualReportSlice.actions;
|
||||||
|
|
||||||
export const selectWrapstodonYear = createAppSelector(
|
// Called on initial load to check if we need to refresh the report state.
|
||||||
[(state) => state.server.getIn(['server', 'wrapstodon'])],
|
|
||||||
(year: unknown) => (typeof year === 'number' && year > 2000 ? year : null),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This kicks everything off, and is called after fetching the server info.
|
|
||||||
export const checkAnnualReport = createAppThunk(
|
export const checkAnnualReport = createAppThunk(
|
||||||
`${annualReportSlice.name}/checkAnnualReport`,
|
`${annualReportSlice.name}/checkAnnualReport`,
|
||||||
(_arg: unknown, { dispatch, getState }) => {
|
(_arg: unknown, { dispatch, getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { state, year } = getState().annualReport;
|
||||||
const me = getState().meta.get('me') as string;
|
const me = getState().meta.get('me') as string;
|
||||||
if (!year || !me) {
|
|
||||||
|
// If we have a state, we only need to fetch it again to poll for changes.
|
||||||
|
const needsStateRefresh = !state || state === 'generating';
|
||||||
|
|
||||||
|
if (!year || !me || !needsStateRefresh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void dispatch(fetchReportState());
|
void dispatch(fetchReportState());
|
||||||
@@ -73,7 +75,7 @@ export const checkAnnualReport = createAppThunk(
|
|||||||
const fetchReportState = createDataLoadingThunk(
|
const fetchReportState = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/fetchReportState`,
|
`${annualReportSlice.name}/fetchReportState`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
@@ -84,8 +86,6 @@ const fetchReportState = createDataLoadingThunk(
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
void dispatch(fetchReportState());
|
void dispatch(fetchReportState());
|
||||||
}, 1_000 * refresh.retry);
|
}, 1_000 * refresh.retry);
|
||||||
} else if (state === 'available') {
|
|
||||||
void dispatch(getReport());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
@@ -97,7 +97,7 @@ const fetchReportState = createDataLoadingThunk(
|
|||||||
export const generateReport = createDataLoadingThunk(
|
export const generateReport = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/generateReport`,
|
`${annualReportSlice.name}/generateReport`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export const generateReport = createDataLoadingThunk(
|
|||||||
export const getReport = createDataLoadingThunk(
|
export const getReport = createDataLoadingThunk(
|
||||||
`${annualReportSlice.name}/getReport`,
|
`${annualReportSlice.name}/getReport`,
|
||||||
async (_arg: unknown, { getState }) => {
|
async (_arg: unknown, { getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const { year } = getState().annualReport;
|
||||||
if (!year) {
|
if (!year) {
|
||||||
throw new Error('Year is not set');
|
throw new Error('Year is not set');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2848,7 +2848,7 @@ a.account__display-name {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: rgb(from var(--color-text-disabled) r g b / 70%);
|
color: var(--color-text-on-disabled);
|
||||||
background: var(--color-bg-disabled);
|
background: var(--color-bg-disabled);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@@ -3994,8 +3994,8 @@ a.account__display-name {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:active,
|
||||||
&:active {
|
&:focus-visible {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4013,14 +4013,7 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--logo {
|
&--logo {
|
||||||
background: transparent;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,25 @@ class AnnualReport
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def state
|
||||||
|
return 'available' if GeneratedAnnualReport.exists?(account_id: @account.id, year: @year)
|
||||||
|
|
||||||
|
async_refresh = AsyncRefresh.new(refresh_key)
|
||||||
|
|
||||||
|
if async_refresh.running?
|
||||||
|
yield async_refresh if block_given?
|
||||||
|
'generating'
|
||||||
|
elsif AnnualReport.current_campaign == @year && eligible?
|
||||||
|
'eligible'
|
||||||
|
else
|
||||||
|
'ineligible'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_key
|
||||||
|
"wrapstodon:#{@account.id}:#{@year}"
|
||||||
|
end
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
|
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class DomainAllow < ApplicationRecord
|
|||||||
def rule_for(domain)
|
def rule_for(domain)
|
||||||
return if domain.blank?
|
return if domain.blank?
|
||||||
|
|
||||||
uri = Addressable::URI.new.tap { |u| u.host = domain.delete('/') }
|
uri = Addressable::URI.new.tap { |u| u.host = domain.strip.delete('/') }
|
||||||
|
|
||||||
find_by(domain: uri.normalized_host)
|
find_by(domain: uri.normalized_host)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
store[:default_content_type] = object_account_user.setting_default_content_type
|
store[:default_content_type] = object_account_user.setting_default_content_type
|
||||||
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
||||||
store[:emoji_style] = object_account_user.settings['web.emoji_style']
|
store[:emoji_style] = object_account_user.settings['web.emoji_style']
|
||||||
|
store[:wrapstodon] = wrapstodon
|
||||||
else
|
else
|
||||||
store[:auto_play_gif] = Setting.auto_play_gif
|
store[:auto_play_gif] = Setting.auto_play_gif
|
||||||
store[:display_media] = Setting.display_media
|
store[:display_media] = Setting.display_media
|
||||||
@@ -110,6 +111,16 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def wrapstodon
|
||||||
|
current_campaign = AnnualReport.current_campaign
|
||||||
|
return if current_campaign.blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
year: current_campaign,
|
||||||
|
state: AnnualReport.new(object.current_account, current_campaign).state,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def default_meta_store
|
def default_meta_store
|
||||||
{
|
{
|
||||||
access_token: object.token,
|
access_token: object.token,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ProcessMentionsService < BaseService
|
|||||||
# Make sure we never mention blocked accounts
|
# Make sure we never mention blocked accounts
|
||||||
unless @current_mentions.empty?
|
unless @current_mentions.empty?
|
||||||
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
|
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
|
||||||
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains))
|
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain))
|
||||||
mentioned_account_ids = @current_mentions.map(&:account_id)
|
mentioned_account_ids = @current_mentions.map(&:account_id)
|
||||||
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))
|
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"vite-plugin-manifest-sri": "^0.2.0",
|
"vite-plugin-manifest-sri": "^0.2.0",
|
||||||
"vite-plugin-pwa": "^1.0.2",
|
"vite-plugin-pwa": "^1.0.2",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^6.0.0",
|
||||||
"wicg-inert": "^3.1.2",
|
"wicg-inert": "^3.1.2",
|
||||||
"workbox-expiration": "^7.3.0",
|
"workbox-expiration": "^7.3.0",
|
||||||
"workbox-routing": "^7.3.0",
|
"workbox-routing": "^7.3.0",
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
"lint-staged": "^16.2.6",
|
"lint-staged": "^16.2.6",
|
||||||
"msw": "^2.12.1",
|
"msw": "^2.12.1",
|
||||||
"msw-storybook-addon": "^2.0.6",
|
"msw-storybook-addon": "^2.0.6",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.57.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"react-test-renderer": "^18.2.0",
|
"react-test-renderer": "^18.2.0",
|
||||||
"storybook": "^10.0.5",
|
"storybook": "^10.0.5",
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ RSpec.describe FollowerAccountsController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when request is signed in and user blocks an account' do
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate :block, account:, target_account: follower_bob
|
||||||
|
sign_in(account.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns followers without blocked' do
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(
|
||||||
|
orderedItems: contain_exactly(
|
||||||
|
include(follow_from_chris.account.id.to_s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when account is permanently suspended' do
|
context 'when account is permanently suspended' do
|
||||||
before do
|
before do
|
||||||
alice.suspend!
|
alice.suspend!
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ RSpec.describe FollowingAccountsController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when request is signed in and user blocks an account' do
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate :block, account:, target_account: followee_bob
|
||||||
|
sign_in(account.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns followers without blocked' do
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(
|
||||||
|
orderedItems: contain_exactly(
|
||||||
|
include(follow_of_chris.target_account.id.to_s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when account is permanently suspended' do
|
context 'when account is permanently suspended' do
|
||||||
before do
|
before do
|
||||||
alice.suspend!
|
alice.suspend!
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ RSpec.describe Admin::AccountModerationNotesHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with account' do
|
context 'with account' do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate.build(:account, id: 123) }
|
||||||
|
|
||||||
it 'returns a labeled avatar link to the account' do
|
it 'returns a labeled avatar link to the account' do
|
||||||
expect(parsed_html.a[:href]).to eq admin_account_path(account.id)
|
expect(parsed_html.a[:href]).to eq admin_account_path(account.id)
|
||||||
@@ -39,7 +39,7 @@ RSpec.describe Admin::AccountModerationNotesHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with account' do
|
context 'with account' do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate.build(:account, id: 123) }
|
||||||
|
|
||||||
it 'returns an inline link to the account' do
|
it 'returns an inline link to the account' do
|
||||||
expect(parsed_html.a[:href]).to eq admin_account_path(account.id)
|
expect(parsed_html.a[:href]).to eq admin_account_path(account.id)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe Admin::DashboardHelper do
|
RSpec.describe Admin::DashboardHelper do
|
||||||
describe 'relevant_account_timestamp' do
|
describe 'relevant_account_timestamp' do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
context 'with an account with older sign in' do
|
context 'with an account with older sign in' do
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
let(:stamp) { 10.days.ago }
|
let(:stamp) { 10.days.ago }
|
||||||
|
|
||||||
it 'returns a time element' do
|
it 'returns a time element' do
|
||||||
@@ -18,8 +19,6 @@ RSpec.describe Admin::DashboardHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with an account with newer sign in' do
|
context 'with an account with newer sign in' do
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
|
|
||||||
it 'returns a time element' do
|
it 'returns a time element' do
|
||||||
account.user.update(current_sign_in_at: 10.hours.ago)
|
account.user.update(current_sign_in_at: 10.hours.ago)
|
||||||
result = helper.relevant_account_timestamp(account)
|
result = helper.relevant_account_timestamp(account)
|
||||||
@@ -29,8 +28,6 @@ RSpec.describe Admin::DashboardHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with an account where the user is pending' do
|
context 'with an account where the user is pending' do
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
|
|
||||||
it 'returns a time element' do
|
it 'returns a time element' do
|
||||||
account.user.update(current_sign_in_at: nil)
|
account.user.update(current_sign_in_at: nil)
|
||||||
account.user.update(approved: false)
|
account.user.update(approved: false)
|
||||||
@@ -42,7 +39,6 @@ RSpec.describe Admin::DashboardHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with an account with a last status value' do
|
context 'with an account with a last status value' do
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
let(:stamp) { 5.minutes.ago }
|
let(:stamp) { 5.minutes.ago }
|
||||||
|
|
||||||
it 'returns a time element' do
|
it 'returns a time element' do
|
||||||
@@ -56,8 +52,6 @@ RSpec.describe Admin::DashboardHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with an account without sign in or last status or pending' do
|
context 'with an account without sign in or last status or pending' do
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
|
|
||||||
it 'returns a time element' do
|
it 'returns a time element' do
|
||||||
account.user.update(current_sign_in_at: nil)
|
account.user.update(current_sign_in_at: nil)
|
||||||
result = helper.relevant_account_timestamp(account)
|
result = helper.relevant_account_timestamp(account)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ RSpec.describe Admin::Trends::StatusesHelper do
|
|||||||
context 'with a status that has emoji' do
|
context 'with a status that has emoji' do
|
||||||
before { Fabricate(:custom_emoji, shortcode: 'florpy') }
|
before { Fabricate(:custom_emoji, shortcode: 'florpy') }
|
||||||
|
|
||||||
let(:status) { Fabricate(:status, text: 'hello there :florpy:') }
|
let(:status) { Fabricate.build(:status, text: 'hello there :florpy:') }
|
||||||
|
|
||||||
it 'renders a correct preview text' do
|
it 'renders a correct preview text' do
|
||||||
result = helper.one_line_preview(status)
|
result = helper.one_line_preview(status)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ RSpec.describe FormattingHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with a spoiler and an emoji and a poll' do
|
context 'with a spoiler and an emoji and a poll' do
|
||||||
let(:status) { Fabricate(:status, text: 'Hello :world: <>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) }
|
let(:status) { Fabricate(:status, text: 'Hello :world: <>', spoiler_text: 'This is a spoiler<>', poll: Fabricate.build(:poll, options: %w(Yes<> No))) }
|
||||||
|
|
||||||
before { Fabricate :custom_emoji, shortcode: 'world' }
|
before { Fabricate :custom_emoji, shortcode: 'world' }
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ RSpec.describe HomeHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with a valid account' do
|
context 'with a valid account' do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate.build(:account) }
|
||||||
|
|
||||||
before { helper.extend controller_helpers }
|
before { helper.extend controller_helpers }
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ require 'rails_helper'
|
|||||||
RSpec.describe MediaComponentHelper do
|
RSpec.describe MediaComponentHelper do
|
||||||
before { helper.extend controller_helpers }
|
before { helper.extend controller_helpers }
|
||||||
|
|
||||||
|
let(:media) { Fabricate.build(:media_attachment, type:, status: Fabricate.build(:status)) }
|
||||||
|
|
||||||
describe 'render_video_component' do
|
describe 'render_video_component' do
|
||||||
let(:media) { Fabricate(:media_attachment, type: :video, status: Fabricate(:status)) }
|
let(:type) { :video }
|
||||||
let(:result) { helper.render_video_component(media.status) }
|
let(:result) { helper.render_video_component(media.status) }
|
||||||
|
|
||||||
it 'renders a react component for the video' do
|
it 'renders a react component for the video' do
|
||||||
@@ -15,7 +17,7 @@ RSpec.describe MediaComponentHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'render_audio_component' do
|
describe 'render_audio_component' do
|
||||||
let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) }
|
let(:type) { :audio }
|
||||||
let(:result) { helper.render_audio_component(media.status) }
|
let(:result) { helper.render_audio_component(media.status) }
|
||||||
|
|
||||||
it 'renders a react component for the audio' do
|
it 'renders a react component for the audio' do
|
||||||
@@ -24,7 +26,7 @@ RSpec.describe MediaComponentHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'render_media_gallery_component' do
|
describe 'render_media_gallery_component' do
|
||||||
let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) }
|
let(:type) { :audio }
|
||||||
let(:result) { helper.render_media_gallery_component(media.status) }
|
let(:result) { helper.render_media_gallery_component(media.status) }
|
||||||
|
|
||||||
it 'renders a react component for the media gallery' do
|
it 'renders a react component for the media gallery' do
|
||||||
|
|||||||
@@ -24,16 +24,13 @@ RSpec.describe StatusesHelper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#media_summary' do
|
describe '#media_summary' do
|
||||||
it 'describes the media on a status' do
|
subject { helper.media_summary(status) }
|
||||||
status = Fabricate :status
|
|
||||||
Fabricate :media_attachment, status: status, type: :video
|
|
||||||
Fabricate :media_attachment, status: status, type: :audio
|
|
||||||
Fabricate :media_attachment, status: status, type: :image
|
|
||||||
|
|
||||||
result = helper.media_summary(status)
|
let(:status) { Fabricate.build :status }
|
||||||
|
|
||||||
expect(result).to eq('Attached: 1 image · 1 video · 1 audio')
|
before { %i(video audio image).each { |type| Fabricate.build :media_attachment, status:, type: } }
|
||||||
end
|
|
||||||
|
it { is_expected.to eq('Attached: 1 image · 1 video · 1 audio') }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'visibility_icon' do
|
describe 'visibility_icon' do
|
||||||
|
|||||||
@@ -3,6 +3,37 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Account::FinderConcern do
|
RSpec.describe Account::FinderConcern do
|
||||||
|
describe '.representative' do
|
||||||
|
context 'with an instance actor using an invalid legacy username' do
|
||||||
|
let(:legacy_value) { 'localhost:3000' }
|
||||||
|
|
||||||
|
before { Account.find(Account::INSTANCE_ACTOR_ID).update_attribute(:username, legacy_value) }
|
||||||
|
|
||||||
|
it 'updates the username to the new value' do
|
||||||
|
expect { Account.representative }
|
||||||
|
.to change { Account.find(Account::INSTANCE_ACTOR_ID).username }.from(legacy_value).to('mastodon.internal')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an instance actor' do
|
||||||
|
before { Account.find(Account::INSTANCE_ACTOR_ID).destroy! }
|
||||||
|
|
||||||
|
it 'creates an instance actor' do
|
||||||
|
expect { Account.representative }
|
||||||
|
.to change(Account.where(id: Account::INSTANCE_ACTOR_ID), :count).from(0).to(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a correctly loaded instance actor' do
|
||||||
|
let(:instance_actor) { Account.find(Account::INSTANCE_ACTOR_ID) }
|
||||||
|
|
||||||
|
it 'returns the instance actor record' do
|
||||||
|
expect(Account.representative)
|
||||||
|
.to eq(instance_actor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'local finders' do
|
describe 'local finders' do
|
||||||
let!(:account) { Fabricate(:account, username: 'Alice') }
|
let!(:account) { Fabricate(:account, username: 'Alice') }
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,27 @@ RSpec.describe DomainAllow do
|
|||||||
it { is_expected.to contain_exactly(allowed_domain.domain, other_allowed_domain.domain) }
|
it { is_expected.to contain_exactly(allowed_domain.domain, other_allowed_domain.domain) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.rule_for' do
|
||||||
|
subject { described_class.rule_for(domain) }
|
||||||
|
|
||||||
|
let(:domain) { 'host.example' }
|
||||||
|
|
||||||
|
context 'with no records' do
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with matching record' do
|
||||||
|
let!(:domain_allow) { Fabricate :domain_allow, domain: }
|
||||||
|
|
||||||
|
it { is_expected.to eq(domain_allow) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when called with non normalized string' do
|
||||||
|
let!(:domain_allow) { Fabricate :domain_allow, domain: }
|
||||||
|
let(:domain) { ' HOST.example/' }
|
||||||
|
|
||||||
|
it { is_expected.to eq(domain_allow) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ RSpec.describe DomainBlock do
|
|||||||
expect(described_class.rule_for('example.com')).to eq block
|
expect(described_class.rule_for('example.com')).to eq block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns most specific rule matching a blocked domain' do
|
||||||
|
_block = Fabricate(:domain_block, domain: 'example.com')
|
||||||
|
blog_block = Fabricate(:domain_block, domain: 'blog.example.com')
|
||||||
|
expect(described_class.rule_for('host.blog.example.com')).to eq blog_block
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns rule matching a blocked domain when string needs normalization' do
|
||||||
|
block = Fabricate(:domain_block, domain: 'example.com')
|
||||||
|
expect(described_class.rule_for(' example.com/')).to eq block
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns a rule matching a subdomain of a blocked domain' do
|
it 'returns a rule matching a subdomain of a blocked domain' do
|
||||||
block = Fabricate(:domain_block, domain: 'example.com')
|
block = Fabricate(:domain_block, domain: 'example.com')
|
||||||
expect(described_class.rule_for('sub.example.com')).to eq block
|
expect(described_class.rule_for('sub.example.com')).to eq block
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe EmailDomainBlock do
|
RSpec.describe EmailDomainBlock do
|
||||||
describe 'block?' do
|
describe 'block?' do
|
||||||
|
subject { described_class.block?(input) }
|
||||||
|
|
||||||
let(:input) { nil }
|
let(:input) { nil }
|
||||||
|
|
||||||
context 'when given an e-mail address' do
|
context 'when given an e-mail address' do
|
||||||
@@ -14,12 +16,12 @@ RSpec.describe EmailDomainBlock do
|
|||||||
|
|
||||||
it 'returns true if the domain is blocked' do
|
it 'returns true if the domain is blocked' do
|
||||||
Fabricate(:email_domain_block, domain: 'example.com')
|
Fabricate(:email_domain_block, domain: 'example.com')
|
||||||
expect(described_class.block?(input)).to be true
|
expect(subject).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false if the domain is not blocked' do
|
it 'returns false if the domain is not blocked' do
|
||||||
Fabricate(:email_domain_block, domain: 'other-example.com')
|
Fabricate(:email_domain_block, domain: 'other-example.com')
|
||||||
expect(described_class.block?(input)).to be false
|
expect(subject).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ RSpec.describe EmailDomainBlock do
|
|||||||
|
|
||||||
it 'returns true if it is a subdomain of a blocked domain' do
|
it 'returns true if it is a subdomain of a blocked domain' do
|
||||||
Fabricate(:email_domain_block, domain: 'example.com')
|
Fabricate(:email_domain_block, domain: 'example.com')
|
||||||
expect(described_class.block?(input)).to be true
|
expect(subject).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -38,8 +40,40 @@ RSpec.describe EmailDomainBlock do
|
|||||||
|
|
||||||
it 'returns true if the domain is blocked' do
|
it 'returns true if the domain is blocked' do
|
||||||
Fabricate(:email_domain_block, domain: 'mail.foo.com')
|
Fabricate(:email_domain_block, domain: 'mail.foo.com')
|
||||||
expect(described_class.block?(input)).to be true
|
expect(subject).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when given nil' do
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given empty string' do
|
||||||
|
let(:input) { '' }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.requires_approval?' do
|
||||||
|
subject { described_class.requires_approval?(input) }
|
||||||
|
|
||||||
|
let(:input) { nil }
|
||||||
|
|
||||||
|
context 'with a matching block requiring approval' do
|
||||||
|
before { Fabricate :email_domain_block, domain: input, allow_with_approval: true }
|
||||||
|
|
||||||
|
let(:input) { 'host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a matching block not requiring approval' do
|
||||||
|
before { Fabricate :email_domain_block, domain: input, allow_with_approval: false }
|
||||||
|
|
||||||
|
let(:input) { 'host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,4 +25,36 @@ RSpec.describe PreviewCardProvider do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.matching_domain' do
|
||||||
|
subject { described_class.matching_domain(domain) }
|
||||||
|
|
||||||
|
let(:domain) { 'host.example' }
|
||||||
|
|
||||||
|
context 'without matching domains' do
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with exact matching domain' do
|
||||||
|
let!(:preview_card_provider) { Fabricate :preview_card_provider, domain: 'host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to eq(preview_card_provider) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with matching domain segment' do
|
||||||
|
let!(:preview_card_provider) { Fabricate :preview_card_provider, domain: 'host.example' }
|
||||||
|
let(:domain) { 'www.blog.host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to eq(preview_card_provider) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple matching records' do
|
||||||
|
let!(:preview_card_provider_more) { Fabricate :preview_card_provider, domain: 'blog.host.example' }
|
||||||
|
let(:domain) { 'www.blog.host.example' }
|
||||||
|
|
||||||
|
before { Fabricate :preview_card_provider, domain: 'host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to eq(preview_card_provider_more) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ RSpec.describe User do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it { is_expected.to allow_value('admin@localhost').for(:email) }
|
it { is_expected.to allow_value('admin@localhost').for(:email) }
|
||||||
|
|
||||||
|
context 'when registration form time is present' do
|
||||||
|
subject { Fabricate.build :user }
|
||||||
|
|
||||||
|
before { stub_const 'RegistrationFormTimeValidator::REGISTRATION_FORM_MIN_TIME', 3.seconds }
|
||||||
|
|
||||||
|
it { is_expected.to allow_value(10.seconds.ago).for(:registration_form_time) }
|
||||||
|
it { is_expected.to_not allow_value(1.second.ago).for(:registration_form_time).against(:base) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Normalizations' do
|
describe 'Normalizations' do
|
||||||
|
|||||||
@@ -96,6 +96,28 @@ RSpec.describe '/api/v1/accounts' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when missing username value' do
|
||||||
|
subject do
|
||||||
|
post '/api/v1/accounts', headers: headers, params: { password: '12345678', email: 'hello@world.tld', agreement: 'true' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity with username error message' do
|
||||||
|
expect { subject }
|
||||||
|
.to not_change(User, :count)
|
||||||
|
.and not_change(Account, :count)
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(422)
|
||||||
|
expect(response.media_type)
|
||||||
|
.to eq('application/json')
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(
|
||||||
|
error: /Validation failed/,
|
||||||
|
details: include(username: contain_exactly(include(error: 'ERR_BLANK', description: /can't be blank/)))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when age verification is enabled' do
|
context 'when age verification is enabled' do
|
||||||
before do
|
before do
|
||||||
Setting.min_age = 16
|
Setting.min_age = 16
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ RSpec.describe ProcessMentionsService do
|
|||||||
let(:account) { Fabricate(:account, username: 'alice') }
|
let(:account) { Fabricate(:account, username: 'alice') }
|
||||||
|
|
||||||
context 'when mentions contain blocked accounts' do
|
context 'when mentions contain blocked accounts' do
|
||||||
let(:non_blocked_account) { Fabricate(:account) }
|
let!(:non_blocked_account) { Fabricate(:account) }
|
||||||
let(:individually_blocked_account) { Fabricate(:account) }
|
let!(:individually_blocked_account) { Fabricate(:account) }
|
||||||
let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') }
|
let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) }
|
||||||
let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) }
|
let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|||||||
30
yarn.lock
30
yarn.lock
@@ -2840,7 +2840,7 @@ __metadata:
|
|||||||
msw: "npm:^2.12.1"
|
msw: "npm:^2.12.1"
|
||||||
msw-storybook-addon: "npm:^2.0.6"
|
msw-storybook-addon: "npm:^2.0.6"
|
||||||
path-complete-extname: "npm:^1.0.0"
|
path-complete-extname: "npm:^1.0.0"
|
||||||
playwright: "npm:^1.56.1"
|
playwright: "npm:^1.57.0"
|
||||||
postcss-preset-env: "npm:^10.1.5"
|
postcss-preset-env: "npm:^10.1.5"
|
||||||
prettier: "npm:^3.3.3"
|
prettier: "npm:^3.3.3"
|
||||||
prop-types: "npm:^15.8.1"
|
prop-types: "npm:^15.8.1"
|
||||||
@@ -2886,7 +2886,7 @@ __metadata:
|
|||||||
vite-plugin-manifest-sri: "npm:^0.2.0"
|
vite-plugin-manifest-sri: "npm:^0.2.0"
|
||||||
vite-plugin-pwa: "npm:^1.0.2"
|
vite-plugin-pwa: "npm:^1.0.2"
|
||||||
vite-plugin-svgr: "npm:^4.3.0"
|
vite-plugin-svgr: "npm:^4.3.0"
|
||||||
vite-tsconfig-paths: "npm:^5.1.4"
|
vite-tsconfig-paths: "npm:^6.0.0"
|
||||||
vitest: "npm:^4.0.5"
|
vitest: "npm:^4.0.5"
|
||||||
wicg-inert: "npm:^3.1.2"
|
wicg-inert: "npm:^3.1.2"
|
||||||
workbox-expiration: "npm:^7.3.0"
|
workbox-expiration: "npm:^7.3.0"
|
||||||
@@ -10558,27 +10558,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"playwright-core@npm:1.56.1":
|
"playwright-core@npm:1.57.0":
|
||||||
version: 1.56.1
|
version: 1.57.0
|
||||||
resolution: "playwright-core@npm:1.56.1"
|
resolution: "playwright-core@npm:1.57.0"
|
||||||
bin:
|
bin:
|
||||||
playwright-core: cli.js
|
playwright-core: cli.js
|
||||||
checksum: 10c0/ffd40142b99c68678b387445d5b42f1fee4ab0b65d983058c37f342e5629f9cdbdac0506ea80a0dfd41a8f9f13345bad54e9a8c35826ef66dc765f4eb3db8da7
|
checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"playwright@npm:^1.56.1":
|
"playwright@npm:^1.57.0":
|
||||||
version: 1.56.1
|
version: 1.57.0
|
||||||
resolution: "playwright@npm:1.56.1"
|
resolution: "playwright@npm:1.57.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
fsevents: "npm:2.3.2"
|
fsevents: "npm:2.3.2"
|
||||||
playwright-core: "npm:1.56.1"
|
playwright-core: "npm:1.57.0"
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
fsevents:
|
fsevents:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
playwright: cli.js
|
playwright: cli.js
|
||||||
checksum: 10c0/8e9965aede86df0f4722063385748498977b219630a40a10d1b82b8bd8d4d4e9b6b65ecbfa024331a30800163161aca292fb6dd7446c531a1ad25f4155625ab4
|
checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -14084,9 +14084,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite-tsconfig-paths@npm:^5.1.4":
|
"vite-tsconfig-paths@npm:^6.0.0":
|
||||||
version: 5.1.4
|
version: 6.0.1
|
||||||
resolution: "vite-tsconfig-paths@npm:5.1.4"
|
resolution: "vite-tsconfig-paths@npm:6.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: "npm:^4.1.1"
|
debug: "npm:^4.1.1"
|
||||||
globrex: "npm:^0.1.2"
|
globrex: "npm:^0.1.2"
|
||||||
@@ -14096,7 +14096,7 @@ __metadata:
|
|||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3
|
checksum: 10c0/c0702f1d2b9d2e3e6ebb44d8e9c27b17b1102e86946ab54b6bbd290419b134e84df4e451b55db973bc97d9de5689df6f67e479633df20244aa0c62ffd0b16e43
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user