From 7c730e9041b5cf1de0741925b7ebbaa43d7d0f22 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 3 Dec 2025 10:39:01 -0500 Subject: [PATCH 01/12] Use `normalizes` API for Tag `display_name` value (#35797) --- app/models/tag.rb | 7 +++---- spec/models/tag_spec.rb | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index c59b0f36a8..9924d132e6 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -67,6 +67,8 @@ class Tag < ApplicationRecord } scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index + normalizes :display_name, with: ->(value) { value.gsub(HASHTAG_INVALID_CHARS_RE, '') } + update_index('tags', :self) def to_param @@ -113,10 +115,7 @@ class Tag < ApplicationRecord names.map do |(normalized_name, display_name)| tag = begin - matching_name(normalized_name).first || create!( - name: normalized_name, - display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '') - ) + matching_name(normalized_name).first || create!(name: normalized_name, display_name:) rescue ActiveRecord::RecordNotUnique find_normalized(normalized_name) end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 9a68ae36d6..06ed7a42c6 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -57,6 +57,11 @@ RSpec.describe Tag do end end + describe 'Normalizations' do + it { is_expected.to normalize(:display_name).from('#HelloWorld').to('HelloWorld') } + it { is_expected.to normalize(:display_name).from('Hello❤️World').to('HelloWorld') } + end + describe 'HASHTAG_RE' do subject { described_class::HASHTAG_RE } From 498e88f059326c4a84d0bdac64cda6f7daa29ca2 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 3 Dec 2025 16:43:26 +0100 Subject: [PATCH 02/12] Fix color contrast issues caused by new theme tokens (#37105) --- app/javascript/styles/mastodon-light/css_variables.scss | 1 + app/javascript/styles/mastodon/components.scss | 4 ++-- app/javascript/styles/mastodon/css_variables.scss | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/javascript/styles/mastodon-light/css_variables.scss b/app/javascript/styles/mastodon-light/css_variables.scss index 70745ec071..a96773f76c 100644 --- a/app/javascript/styles/mastodon-light/css_variables.scss +++ b/app/javascript/styles/mastodon-light/css_variables.scss @@ -39,6 +39,7 @@ var(--color-text-brand) ); --color-text-on-brand-base: var(--color-white); + --color-text-brand-on-inverted: var(--color-indigo-400); --color-text-error: var(--color-red-600); --color-text-on-error-base: var(--color-white); --color-text-warning: var(--color-yellow-600); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 01de2c7d7a..ed2403b045 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -10267,7 +10267,7 @@ noscript { background: transparent; text-transform: uppercase; cursor: pointer; - color: var(--color-text-brand); + color: var(--color-text-brand-on-inverted); font-weight: 700; border-radius: 4px; padding: 0 4px; @@ -10751,7 +10751,7 @@ noscript { box-sizing: border-box; font-size: 14px; color: var(--color-text-secondary); - background: var(--color-bg-tertiary); + background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-top: 0; border-radius: 0 0 8px 8px; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index b270bd337f..98b61d2f0b 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -39,6 +39,7 @@ var(--color-text-brand) ); --color-text-on-brand-base: var(--color-white); + --color-text-brand-on-inverted: var(--color-indigo-600); --color-text-error: var(--color-red-500); --color-text-on-error-base: var(--color-white); --color-text-warning: var(--color-yellow-400); @@ -60,7 +61,7 @@ // Neutrals --color-bg-primary: var(--color-grey-950); - --overlay-strength-secondary: 10%; + --overlay-strength-secondary: 8%; --color-bg-secondary-base: var(--color-indigo-200); --color-bg-secondary: #{utils.css-alpha( var(--color-bg-secondary-base), From 31c392b1bc816814d0ba3058ea87eb16bedec321 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 3 Dec 2025 17:25:36 +0100 Subject: [PATCH 03/12] Wrapstodon modal with new share button (#37109) --- .../features/annual_report/archetype.tsx | 64 ++++++------ .../mastodon/features/annual_report/index.tsx | 98 +++++++++---------- .../features/annual_report/timeline.tsx | 9 +- .../ui/components/annual_report_modal.tsx | 7 +- app/javascript/mastodon/locales/en.json | 1 + .../mastodon/reducers/slices/annual_report.ts | 10 +- 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/app/javascript/mastodon/features/annual_report/archetype.tsx b/app/javascript/mastodon/features/annual_report/archetype.tsx index fffbc1803e..0c416c30c2 100644 --- a/app/javascript/mastodon/features/annual_report/archetype.tsx +++ b/app/javascript/mastodon/features/annual_report/archetype.tsx @@ -1,68 +1,64 @@ -import { FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import booster from '@/images/archetypes/booster.png'; import lurker from '@/images/archetypes/lurker.png'; import oracle from '@/images/archetypes/oracle.png'; import pollster from '@/images/archetypes/pollster.png'; import replier from '@/images/archetypes/replier.png'; -import type { Archetype as ArchetypeData } from 'mastodon/models/annual_report'; +import type { Archetype as ArchetypeData } from '@/mastodon/models/annual_report'; + +export const archetypeNames = defineMessages({ + booster: { + id: 'annual_report.summary.archetype.booster', + defaultMessage: 'The cool-hunter', + }, + replier: { + id: 'annual_report.summary.archetype.replier', + defaultMessage: 'The social butterfly', + }, + pollster: { + id: 'annual_report.summary.archetype.pollster', + defaultMessage: 'The pollster', + }, + lurker: { + id: 'annual_report.summary.archetype.lurker', + defaultMessage: 'The lurker', + }, + oracle: { + id: 'annual_report.summary.archetype.oracle', + defaultMessage: 'The oracle', + }, +}); export const Archetype: React.FC<{ data: ArchetypeData; }> = ({ data }) => { - let illustration, label; + const intl = useIntl(); + let illustration; switch (data) { case 'booster': illustration = booster; - label = ( - - ); break; case 'replier': illustration = replier; - label = ( - - ); break; case 'pollster': illustration = pollster; - label = ( - - ); break; case 'lurker': illustration = lurker; - label = ( - - ); break; case 'oracle': illustration = oracle; - label = ( - - ); break; } return (
-
{label}
+
+ {intl.formatMessage(archetypeNames[data])} +
); diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index 9c2c9d71c0..e9f0b5f2d7 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -1,68 +1,38 @@ -import { useState, useEffect } from 'react'; +import { useCallback } from 'react'; +import type { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; -import { - importFetchedStatuses, - importFetchedAccounts, -} from 'mastodon/actions/importer'; -import { apiRequestGet, apiRequestPost } from 'mastodon/api'; -import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import { me } from 'mastodon/initial_state'; -import type { Account } from 'mastodon/models/account'; -import type { AnnualReport as AnnualReportData } from 'mastodon/models/annual_report'; -import type { Status } from 'mastodon/models/status'; -import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { focusCompose, resetCompose } from '@/mastodon/actions/compose'; +import { closeModal } from '@/mastodon/actions/modal'; +import { Button } from '@/mastodon/components/button'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { me } from '@/mastodon/initial_state'; +import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; -import { Archetype } from './archetype'; +import { Archetype, archetypeNames } from './archetype'; import { Followers } from './followers'; import { HighlightedPost } from './highlighted_post'; import { MostUsedHashtag } from './most_used_hashtag'; import { NewPosts } from './new_posts'; -import { Percentile } from './percentile'; -interface AnnualReportResponse { - annual_reports: AnnualReportData[]; - accounts: Account[]; - statuses: Status[]; -} +const shareMessage = defineMessage({ + id: 'annual_report.summary.share_message', + defaultMessage: 'I got the {archetype} archetype!', +}); -export const AnnualReport: React.FC<{ - year: string; -}> = ({ year }) => { - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); +// Share = false when using the embedded version of the report. +export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => { const currentAccount = useAppSelector((state) => me ? state.accounts.get(me) : undefined, ); - const dispatch = useAppDispatch(); + const report = useAppSelector((state) => state.annualReport.report); - useEffect(() => { - apiRequestGet(`v1/annual_reports/${year}`) - .then((data) => { - dispatch(importFetchedStatuses(data.statuses)); - dispatch(importFetchedAccounts(data.accounts)); - - setResponse(data); - setLoading(false); - - return apiRequestPost(`v1/annual_reports/${year}/read`); - }) - .catch(() => { - setLoading(false); - }); - }, [dispatch, year, setResponse, setLoading]); - - if (loading) { + if (!report) { return ; } - const report = response?.annual_reports[0]; - - if (!report || report.schema_version !== 1) { - return null; - } - return (
@@ -89,9 +59,37 @@ export const AnnualReport: React.FC<{ total={currentAccount?.followers_count} /> - + {share && }
); }; + +const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const handleShareClick = useCallback(() => { + // Generate the share message. + const archetypeName = intl.formatMessage( + archetypeNames[report.data.archetype], + ); + const shareLines = [ + intl.formatMessage(shareMessage, { + archetype: archetypeName, + }), + ]; + // Share URL is only available for schema version 2. + if (report.schema_version === 2 && report.share_url) { + shareLines.push(report.share_url); + } + shareLines.push(`#Wrapstodon${report.year}`); + + // Reset the composer and focus it with the share message, then close the modal. + dispatch(resetCompose()); + dispatch(focusCompose(shareLines.join('\n\n'))); + dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); + }, [report, intl, dispatch]); + + return