diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bb26b514..399b2fe084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [4.5.3] - 2025-12-08 + +### Security + +- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8)) + +### Fixed + +- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire) +- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima) +- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire) +- Fix creation of duplicate conversations (#37108 by @oneiros) +- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima) +- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire) +- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire) +- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion) +- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire) +- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire) +- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire) + ## [4.5.2] - 2025-11-20 ### Changed diff --git a/Gemfile.lock b/Gemfile.lock index 43d9cc142b..8a94dd6075 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -855,7 +855,7 @@ GEM unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) climate_control - test-prof (1.4.4) + test-prof (1.5.0) thor (1.4.0) tilt (2.6.1) timeout (0.4.3) diff --git a/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt b/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt new file mode 100644 index 0000000000..63c1c98e1e --- /dev/null +++ b/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt @@ -0,0 +1,100 @@ +Below you'll find the original License file for the Silkscreen font. +The file used on Mastodon is a custom file subset to only include the +characters "Wrapstodon 0123456789" using the Font Squirrel Font-face Generator +(https://www.fontsquirrel.com/tools/webfont-generator) + +----------------------------------------------------------- + +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 b/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 new file mode 100644 index 0000000000..3b7ba43e9c Binary files /dev/null and b/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 differ diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index f1d8dd9a97..056e7d7b23 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,12 +1,13 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report'; import api, { getLinks } from 'mastodon/api'; import { compareId } from 'mastodon/compare_id'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; -import {timelineDelete} from './timelines_typed'; +import { timelineDelete } from './timelines_typed'; export { disconnectTimeline } from './timelines_typed'; @@ -24,9 +25,16 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; export const TIMELINE_INSERT = 'TIMELINE_INSERT'; +// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions'; export const TIMELINE_GAP = null; +export const TIMELINE_NON_STATUS_MARKERS = [ + TIMELINE_GAP, + TIMELINE_SUGGESTIONS, + TIMELINE_WRAPSTODON, +]; + export const loadPending = timeline => ({ type: TIMELINE_LOAD_PENDING, timeline, @@ -124,6 +132,7 @@ export function expandTimeline(timelineId, path, params = {}) { if (timelineId === 'home') { dispatch(submitMarkers()); + dispatch(reinsertAnnualReport()) } } catch(error) { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); diff --git a/app/javascript/mastodon/actions/timelines_typed.ts b/app/javascript/mastodon/actions/timelines_typed.ts index 07d82b2f01..e846882660 100644 --- a/app/javascript/mastodon/actions/timelines_typed.ts +++ b/app/javascript/mastodon/actions/timelines_typed.ts @@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { TIMELINE_NON_STATUS_MARKERS } from './timelines'; + +export function isNonStatusId(value: unknown) { + return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null); +} + export const disconnectTimeline = createAction( 'timeline/disconnect', ({ timeline }: { timeline: string }) => ({ diff --git a/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx new file mode 100644 index 0000000000..3ccaceae6c --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { + accountFactoryState, + annualReportFactory, + statusFactoryState, +} from '@/testing/factories'; + +import { AnnualReport } from '.'; + +const SAMPLE_HASHTAG = { + name: 'Mastodon', + count: 14, +}; + +const meta = { + title: 'Components/AnnualReport', + component: AnnualReport, + args: { + context: 'standalone', + }, + parameters: { + state: { + accounts: { + '1': accountFactoryState({ display_name: 'Freddie Fruitbat' }), + }, + statuses: { + '1': statusFactoryState(), + }, + annualReport: annualReportFactory({ + top_hashtag: SAMPLE_HASHTAG, + }), + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Standalone: Story = { + args: { + context: 'standalone', + }, +}; + +export const InModal: Story = { + args: { + context: 'modal', + }, +}; + +export const ArchetypeOracle: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'oracle', + top_hashtag: SAMPLE_HASHTAG, + }), + }, + }, +}; + +export const NoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'booster', + }), + }, + }, +}; + +export const NoNewPosts: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'pollster', + top_hashtag: SAMPLE_HASHTAG, + without_posts: true, + }), + }, + }, +}; + +export const NoNewPostsNoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'replier', + without_posts: true, + }), + }, + }, +}; diff --git a/app/javascript/mastodon/features/annual_report/archetype.tsx b/app/javascript/mastodon/features/annual_report/archetype.tsx index 2163a19eeb..660a1cf29d 100644 --- a/app/javascript/mastodon/features/annual_report/archetype.tsx +++ b/app/javascript/mastodon/features/annual_report/archetype.tsx @@ -12,7 +12,6 @@ import replier from '@/images/archetypes/replier.png'; import space_elements from '@/images/archetypes/space_elements.png'; import { Avatar } from '@/mastodon/components/avatar'; import { Button } from '@/mastodon/components/button'; -import { me } from '@/mastodon/initial_state'; import type { Account } from '@/mastodon/models/account'; import type { AnnualReport, @@ -112,11 +111,11 @@ const illustrations = { export const Archetype: React.FC<{ report: AnnualReport; account?: Account; - canShare: boolean; -}> = ({ report, account, canShare }) => { + context: 'modal' | 'standalone'; +}> = ({ report, account, context }) => { const intl = useIntl(); const wrapperRef = useRef(null); - const isSelfView = account?.id === me; + const isSelfView = context === 'modal'; const [isRevealed, setIsRevealed] = useState(!isSelfView); const reveal = useCallback(() => { @@ -209,7 +208,7 @@ export const Archetype: React.FC<{ /> )} - {isRevealed && canShare && } + {isRevealed && isSelfView && } ); }; diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx index 5ce4947609..2ff8597aa2 100644 --- a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx +++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx @@ -19,7 +19,8 @@ const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any; export const HighlightedPost: React.FC<{ data: TopStatuses; -}> = ({ data }) => { + context: 'modal' | 'standalone'; +}> = ({ data, context }) => { const { by_reblogs, by_favourites, by_replies } = data; const statusId = by_reblogs || by_favourites || by_replies; @@ -68,10 +69,10 @@ export const HighlightedPost: React.FC<{ defaultMessage='Most popular post' /> -

{label}

+ {context === 'modal' &&

{label}

} - + ); }; diff --git a/app/javascript/mastodon/features/annual_report/index.module.scss b/app/javascript/mastodon/features/annual_report/index.module.scss index 0258f9c798..95ebb72729 100644 --- a/app/javascript/mastodon/features/annual_report/index.module.scss +++ b/app/javascript/mastodon/features/annual_report/index.module.scss @@ -1,3 +1,14 @@ +$mobile-breakpoint: 540px; + +@font-face { + font-family: silkscreen-wrapstodon; + src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2') + format('woff2'); + font-weight: normal; + font-display: swap; + font-style: normal; +} + .modalWrapper { position: absolute; inset: 0; @@ -9,22 +20,33 @@ pointer-events: none; scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary); + @media (width < $mobile-breakpoint) { + padding-inline: 10px; + } + .loading-indicator .circular-progress { color: var(--lime); } } .closeButton { - position: absolute; - top: 24px; - right: 24px; - padding: 8px; - border-radius: 100%; - --default-icon-color: var(--color-bg-primary); --default-bg-color: var(--color-text-primary); --hover-icon-color: var(--color-bg-primary); --hover-bg-color: var(--color-text-primary); + --corner-distance: 18px; + + position: absolute; + top: var(--corner-distance); + right: var(--corner-distance); + padding: 8px; + border-radius: 100%; + + @media (width < $mobile-breakpoint) { + --corner-distance: 16px; + + padding: 4px; + } } .wrapper { @@ -45,8 +67,10 @@ var(--color-bg-primary); border-radius: 40px; - @media (width < 600px) { - padding: 12px; + @media (width < $mobile-breakpoint) { + padding-inline: 12px; + padding-bottom: 12px; + border-radius: 28px; } &::after { @@ -65,13 +89,16 @@ text-align: center; h1 { - font-family: monospace; - text-transform: uppercase; - letter-spacing: 0.15em; - font-size: 30px; - font-weight: 600; - line-height: 1.5; + font-family: silkscreen-wrapstodon, monospace; + font-size: 28px; + line-height: 1; margin-bottom: 8px; + padding-inline: 40px; // Prevent overlap with close button + + @media (width < $mobile-breakpoint) { + font-size: 22px; + margin-bottom: 4px; + } } p { @@ -150,6 +177,10 @@ font-weight: 500; line-height: 1; overflow-wrap: break-word; + + @media (width < $mobile-breakpoint) { + font-size: 24px; + } } .mostBoostedPost { @@ -166,7 +197,7 @@ 'followers hashtag' 'new-posts hashtag'; - @media (width < 680px) { + @media (width < $mobile-breakpoint) { grid-template-columns: 1fr 1fr; grid-template-areas: 'followers new-posts' @@ -191,7 +222,7 @@ grid-template-columns: 1fr 2fr; grid-template-areas: 'number hashtag'; - @media (width < 680px) { + @media (width < $mobile-breakpoint) { grid-template-areas: 'number number' 'hashtag hashtag'; @@ -230,6 +261,10 @@ flex-direction: column; align-items: center; gap: 12px; + + p { + max-width: 460px; + } } .archetypeArtboard { diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index b02e8fb898..91fd02c7a7 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { FC } from 'react'; -import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; +import { defineMessage, useIntl } from 'react-intl'; import { useLocation } from 'react-router'; @@ -28,7 +28,6 @@ export const shareMessage = defineMessage({ defaultMessage: 'I got the {archetype} archetype!', }); -// Share = false when using the embedded version of the report. export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ context = 'standalone', }) => { @@ -67,23 +66,16 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ 0, ); - const newFollowerCount = report.data.time_series.reduce( - (sum, item) => sum + item.followers, - 0, - ); + const newFollowerCount = + context === 'modal' && + report.data.time_series.reduce((sum, item) => sum + item.followers, 0); const topHashtag = report.data.top_hashtags[0]; return (
-

- -

+

Wrapstodon {report.year}

{account &&

@{account.acct}

} {context === 'modal' && ( = ({
- +
= ({ > {!!newFollowerCount && } {!!newPostCount && } - {topHashtag && } + {topHashtag && ( + + )}
- +
); diff --git a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx index 9a0720d8ab..5fe386bf2b 100644 --- a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx +++ b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx @@ -8,7 +8,9 @@ import styles from './index.module.scss'; export const MostUsedHashtag: React.FC<{ hashtag: NameAndCount; -}> = ({ hashtag }) => { + name: string | undefined; + context: 'modal' | 'standalone'; +}> = ({ hashtag, name, context }) => { return (
#{hashtag.name}

- + {context === 'modal' ? ( + + ) : ( + name && ( + + ) + )}

); diff --git a/app/javascript/mastodon/features/annual_report/shared_page.module.css b/app/javascript/mastodon/features/annual_report/shared_page.module.css index 99b713c05d..458f4f9558 100644 --- a/app/javascript/mastodon/features/annual_report/shared_page.module.css +++ b/app/javascript/mastodon/features/annual_report/shared_page.module.css @@ -1,11 +1,12 @@ .wrapper { max-width: max-content; - margin: 40px auto; + margin-inline: auto; + padding: 40px 10px; } .footer { text-align: center; - margin-top: 1rem; + margin-top: 2rem; display: flex; flex-direction: column; gap: 0.75rem; diff --git a/app/javascript/mastodon/features/annual_report/shared_page.tsx b/app/javascript/mastodon/features/annual_report/shared_page.tsx index b2e29d0dc7..9749e4d62b 100644 --- a/app/javascript/mastodon/features/annual_report/shared_page.tsx +++ b/app/javascript/mastodon/features/annual_report/shared_page.tsx @@ -1,5 +1,7 @@ import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + import { IconLogo } from '@/mastodon/components/logo'; import { AnnualReport } from './index'; @@ -11,7 +13,11 @@ export const WrapstodonSharedPage: FC = () => {
- Generated with ♥ by the Mastodon team +
); diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 66e3b91c7b..1e21730a00 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -4,10 +4,10 @@ import { connect } from 'react-redux'; import { debounce } from 'lodash'; -import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/mastodon/actions/timelines'; +import { scrollTopTimeline, loadPending } from '@/mastodon/actions/timelines'; +import { isNonStatusId } from '@/mastodon/actions/timelines_typed'; import StatusList from '@/mastodon/components/status_list'; import { me } from '@/mastodon/initial_state'; -import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report'; const makeGetStatusIds = (pending = false) => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), @@ -15,7 +15,7 @@ const makeGetStatusIds = (pending = false) => createSelector([ (state) => state.get('statuses'), ], (columnSettings, statusIds, statuses) => { return statusIds.filter(id => { - if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true; + if (isNonStatusId(id)) return true; const statusForId = statuses.get(id); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 170a5a0836..758ca3d012 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -117,6 +117,7 @@ "annual_report.announcement.action_view": "View my Wrapstodon", "annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.", "annual_report.announcement.title": "Wrapstodon {year} has arrived", + "annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team", "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.name": "The Archer", @@ -146,11 +147,11 @@ "annual_report.summary.most_used_app.most_used_app": "most used app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag", "annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.", "annual_report.summary.new_posts.new_posts": "new posts", "annual_report.summary.percentile.text": "That puts you in the topof {domain} users.", "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", "annual_report.summary.share_message": "I got the {archetype} archetype!", - "annual_report.summary.title": "Wrapstodon {year}", "attachments_list.unprocessed": "(unprocessed)", "audio.hide": "Hide audio", "block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.", diff --git a/app/javascript/mastodon/models/annual_report.ts b/app/javascript/mastodon/models/annual_report.ts index 712f406310..863debfc1f 100644 --- a/app/javascript/mastodon/models/annual_report.ts +++ b/app/javascript/mastodon/models/annual_report.ts @@ -16,9 +16,9 @@ export interface TimeSeriesMonth { } export interface TopStatuses { - by_reblogs: number; - by_favourites: number; - by_replies: number; + by_reblogs: string; + by_favourites: string; + by_replies: string; } export type Archetype = diff --git a/app/javascript/mastodon/reducers/slices/annual_report.ts b/app/javascript/mastodon/reducers/slices/annual_report.ts index c01b8f7995..3ad18f8ec1 100644 --- a/app/javascript/mastodon/reducers/slices/annual_report.ts +++ b/app/javascript/mastodon/reducers/slices/annual_report.ts @@ -6,6 +6,7 @@ import { importFetchedStatuses, } from '@/mastodon/actions/importer'; import { insertIntoTimeline } from '@/mastodon/actions/timelines'; +import { timelineDelete } from '@/mastodon/actions/timelines_typed'; import type { ApiAnnualReportState } from '@/mastodon/api/annual_report'; import { apiGetAnnualReport, @@ -78,6 +79,25 @@ export const checkAnnualReport = createAppThunk( }, ); +export const reinsertAnnualReport = createAppThunk( + `${annualReportSlice.name}/reinsertAnnualReport`, + (_arg: unknown, { dispatch, getState }) => { + dispatch( + timelineDelete({ + statusId: TIMELINE_WRAPSTODON, + accountId: '', + references: [], + reblogOf: null, + }), + ); + const { state } = getState().annualReport; + if (!state || state === 'ineligible') { + return; + } + dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1)); + }, +); + const fetchReportState = createDataLoadingThunk( `${annualReportSlice.name}/fetchReportState`, async (_arg: unknown, { getState }) => { diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index b07281ab87..e915fa7070 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -1,6 +1,6 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import { timelineDelete } from 'mastodon/actions/timelines_typed'; +import { timelineDelete, isNonStatusId } from 'mastodon/actions/timelines_typed'; import { blockAccountSuccess, @@ -19,7 +19,6 @@ import { TIMELINE_MARK_AS_PARTIAL, TIMELINE_INSERT, TIMELINE_GAP, - TIMELINE_SUGGESTIONS, disconnectTimeline, } from '../actions/timelines'; import { compareId } from '../compare_id'; @@ -36,7 +35,6 @@ const initialTimeline = ImmutableMap({ items: ImmutableList(), }); -const isPlaceholder = value => value === TIMELINE_GAP || value === TIMELINE_SUGGESTIONS; const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { // This method is pretty tricky because: @@ -69,20 +67,20 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // First, find the furthest (if properly sorted, oldest) item in the timeline that is // newer than the oldest fetched one, as it's most likely that it delimits the gap. // Start the gap *after* that item. - const lastIndex = oldIds.findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.last()) >= 0) + 1; + const lastIndex = oldIds.findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.last()) >= 0) + 1; // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. - const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.first()) > 0) + 1; + const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.first()) > 0) + 1; let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { // It is possible, though unlikely, that the slice we are replacing contains items older // than the elements we got from the API. Get them and add them back at the back of the // slice. - const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isPlaceholder(id) && compareId(id, newIds.last()) < 0); + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isNonStatusId(id) && compareId(id, newIds.last()) < 0); insertedIds.union(olderIds); // Make sure we aren't inserting duplicates diff --git a/app/javascript/styles/mastodon/theme/index.scss b/app/javascript/styles/mastodon/theme/index.scss index 8e275e514a..a907299887 100644 --- a/app/javascript/styles/mastodon/theme/index.scss +++ b/app/javascript/styles/mastodon/theme/index.scss @@ -6,7 +6,7 @@ html { @include base.palette; - &[data-user-theme='system'] { + &:where([data-user-theme='system']) { color-scheme: dark light; @media (prefers-color-scheme: dark) { diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index f86aa772dc..95afb41028 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List } from 'immutable'; import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships'; import type { ApiStatusJSON } from '@/mastodon/api_types/statuses'; @@ -7,6 +7,7 @@ import type { UnicodeEmojiData, } from '@/mastodon/features/emoji/types'; import { createAccountFromServerJSON } from '@/mastodon/models/account'; +import type { AnnualReport } from '@/mastodon/models/annual_report'; import type { Status } from '@/mastodon/models/status'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; @@ -75,16 +76,18 @@ export const statusFactory: FactoryFunction = ({ mentions: [], tags: [], emojis: [], - content: '

This is a test status.

', + contentHtml: '

This is a test status.

', ...data, }); export const statusFactoryState = ( options: FactoryOptions = {}, ) => - ImmutableMap( - statusFactory(options) as unknown as Record, - ) as unknown as Status; + ImmutableMap({ + ...(statusFactory(options) as unknown as Record), + account: options.account?.id ?? '1', + tags: List(options.tags), + }) as unknown as Status; export const relationshipsFactory: FactoryFunction = ({ id, @@ -130,3 +133,119 @@ export function customEmojiFactory( ...data, }; } + +interface AnnualReportState { + state: 'available'; + report: AnnualReport; +} + +interface AnnualReportFactoryOptions { + account_id?: string; + status_id?: string; + archetype?: AnnualReport['data']['archetype']; + year?: number; + top_hashtag?: AnnualReport['data']['top_hashtags'][0]; + without_posts?: boolean; +} + +export function annualReportFactory({ + account_id = '1', + status_id = '1', + archetype = 'lurker', + year, + top_hashtag, + without_posts = false, +}: AnnualReportFactoryOptions = {}): AnnualReportState { + return { + state: 'available', + report: { + schema_version: 2, + share_url: '#', + account_id, + year: year ?? 2025, + data: { + archetype, + time_series: [ + { + month: 1, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 2, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 3, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 4, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 5, + statuses: without_posts ? 0 : 1, + followers: 1, + following: 3, + }, + { + month: 6, + statuses: without_posts ? 0 : 7, + followers: 1, + following: 0, + }, + { + month: 7, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 8, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 9, + statuses: without_posts ? 0 : 11, + followers: 0, + following: 1, + }, + { + month: 10, + statuses: without_posts ? 0 : 12, + followers: 0, + following: 1, + }, + { + month: 11, + statuses: without_posts ? 0 : 6, + followers: 0, + following: 1, + }, + { + month: 12, + statuses: without_posts ? 0 : 4, + followers: 0, + following: 0, + }, + ], + top_hashtags: top_hashtag ? [top_hashtag] : [], + top_statuses: { + by_reblogs: status_id, + by_replies: status_id, + by_favourites: status_id, + }, + }, + }, + }; +} diff --git a/app/lib/annual_report/time_series.rb b/app/lib/annual_report/time_series.rb index 3f9f0d52e8..fc2c5e2ce4 100644 --- a/app/lib/annual_report/time_series.rb +++ b/app/lib/annual_report/time_series.rb @@ -3,29 +3,24 @@ class AnnualReport::TimeSeries < AnnualReport::Source def generate { - time_series: (1..12).map do |month| - { - month: month, - statuses: statuses_per_month[month] || 0, - following: following_per_month[month] || 0, - followers: followers_per_month[month] || 0, - } - end, + time_series: [ + { + month: 12, + statuses: statuses_this_year, + followers: followers_this_year, + }, + ], } end private - def statuses_per_month - @statuses_per_month ||= report_statuses.group(:period).pluck(date_part_month.as('period'), Arel.star.count).to_h + def statuses_this_year + @statuses_this_year ||= report_statuses.count end - def following_per_month - @following_per_month ||= annual_relationships_by_month(@account.active_relationships) - end - - def followers_per_month - @followers_per_month ||= annual_relationships_by_month(@account.passive_relationships) + def followers_this_year + @followers_this_year ||= @account.passive_relationships.where(created_in_year, @year).count end def date_part_month @@ -34,14 +29,6 @@ class AnnualReport::TimeSeries < AnnualReport::Source SQL end - def annual_relationships_by_month(relationships) - relationships - .where(created_in_year, @year) - .group(:period) - .pluck(date_part_month.as('period'), Arel.star.count) - .to_h - end - def created_in_year Arel.sql(<<~SQL.squish) DATE_PART('year', created_at) = ? diff --git a/app/views/wrapstodon/_og_description.html.haml b/app/views/wrapstodon/_og_description.html.haml new file mode 100644 index 0000000000..7b6e04cb8e --- /dev/null +++ b/app/views/wrapstodon/_og_description.html.haml @@ -0,0 +1,4 @@ +- description = t('wrapstodon.description', name: display_name(account)) + +%meta{ name: 'description', content: description }/ += opengraph 'og:description', description diff --git a/app/views/wrapstodon/show.html.haml b/app/views/wrapstodon/show.html.haml index 809b931b27..3a8f531f98 100644 --- a/app/views/wrapstodon/show.html.haml +++ b/app/views/wrapstodon/show.html.haml @@ -4,8 +4,12 @@ %meta{ name: 'robots', content: 'noindex, noarchive' }/ = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'article' + = opengraph 'og:title', t('wrapstodon.title', name: display_name(@account), year: @generated_annual_report.year) = opengraph 'profile:username', acct(@account)[1..] + = render 'og_description', account: @account + = flavoured_vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous' - content_for :html_classes, 'theme-dark' diff --git a/config/locales/en.yml b/config/locales/en.yml index 0c44a7c3bf..f2b6de3a77 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2187,4 +2187,5 @@ en: otp_required: To use security keys please enable two-factor authentication first. registered_on: Registered on %{date} wrapstodon: + description: See how %{name} used Mastodon this year! title: Wrapstodon %{year} for %{name} diff --git a/docker-compose.yml b/docker-compose.yml index 0a75e96c5d..fae93955f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/glitch-soc/mastodon:v4.5.2 + image: ghcr.io/glitch-soc/mastodon:v4.5.3 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.2 + image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.3 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/glitch-soc/mastodon:v4.5.2 + image: ghcr.io/glitch-soc/mastodon:v4.5.3 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/spec/lib/annual_report/time_series_spec.rb b/spec/lib/annual_report/time_series_spec.rb index 219d6c0834..046ac5b202 100644 --- a/spec/lib/annual_report/time_series_spec.rb +++ b/spec/lib/annual_report/time_series_spec.rb @@ -13,7 +13,7 @@ RSpec.describe AnnualReport::TimeSeries do expect(subject.generate) .to include( time_series: match( - include(followers: 0, following: 0, month: 1, statuses: 0) + include(followers: 0, month: 12, statuses: 0) ) ) end @@ -37,7 +37,7 @@ RSpec.describe AnnualReport::TimeSeries do expect(subject.generate) .to include( time_series: match( - include(followers: 1, following: 1, month: 1, statuses: 1) + include(followers: 1, month: 12, statuses: 1) ) ) end diff --git a/yarn.lock b/yarn.lock index 0765267468..3edf67c664 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1670,6 +1670,15 @@ __metadata: languageName: node linkType: hard +"@csstools/postcss-position-area-property@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-position-area-property@npm:1.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/38f770454d46bfed01d43a3f5e7ac07d3111399b374a7198ae6503cdb6288e410c7b4199f5a7af8f16aeb688216445ade97be417c084313d6c56f55e50d34559 + languageName: node + linkType: hard + "@csstools/postcss-progressive-custom-properties@npm:^4.2.1": version: 4.2.1 resolution: "@csstools/postcss-progressive-custom-properties@npm:4.2.1" @@ -1746,6 +1755,18 @@ __metadata: languageName: node linkType: hard +"@csstools/postcss-system-ui-font-family@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-system-ui-font-family@npm:1.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/6a81761ae3cae643659b1416a7a892cf1505474896193b8abc26cff319cb6b1a20b64c5330d64019fba458e058da3abc9407d0ebf0c102289c0b79ef99b4c6d6 + languageName: node + linkType: hard + "@csstools/postcss-text-decoration-shorthand@npm:^4.0.3": version: 4.0.3 resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.3" @@ -5399,13 +5420,13 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:^10.4.21": - version: 10.4.21 - resolution: "autoprefixer@npm:10.4.21" +"autoprefixer@npm:^10.4.22": + version: 10.4.22 + resolution: "autoprefixer@npm:10.4.22" dependencies: - browserslist: "npm:^4.24.4" - caniuse-lite: "npm:^1.0.30001702" - fraction.js: "npm:^4.3.7" + browserslist: "npm:^4.27.0" + caniuse-lite: "npm:^1.0.30001754" + fraction.js: "npm:^5.3.4" normalize-range: "npm:^0.1.2" picocolors: "npm:^1.1.1" postcss-value-parser: "npm:^4.2.0" @@ -5413,7 +5434,7 @@ __metadata: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 10c0/de5b71d26d0baff4bbfb3d59f7cf7114a6030c9eeb66167acf49a32c5b61c68e308f1e0f869d92334436a221035d08b51cd1b2f2c4689b8d955149423c16d4d4 + checksum: 10c0/2ae8d135af2deaaa5065a3a466c877787373c0ed766b8a8e8259d7871db79c1a7e1d9f6c9541c54fa95647511d3c2066bb08a30160e58c9bfa75506f9c18f3aa languageName: node linkType: hard @@ -5538,12 +5559,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.3": - version: 2.8.6 - resolution: "baseline-browser-mapping@npm:2.8.6" +"baseline-browser-mapping@npm:^2.9.0": + version: 2.9.2 + resolution: "baseline-browser-mapping@npm:2.9.2" bin: baseline-browser-mapping: dist/cli.js - checksum: 10c0/ea628db5048d1e5c0251d4783e0496f5ce8de7a0e20ea29c8876611cb0acf58ffc76bf6561786c6388db22f130646e3ecb91eebc1c03954552a21d38fa38320f + checksum: 10c0/4f9be09e20261ed26f19e9b95454dcb8d8371b87983c57cd9f70b9572e9b3053577f0d8d6d91297bdb605337747680686e22f62522a6e57ae2488fcacf641188 languageName: node linkType: hard @@ -5635,18 +5656,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.1, browserslist@npm:^4.26.0": - version: 4.26.2 - resolution: "browserslist@npm:4.26.2" +"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.27.0, browserslist@npm:^4.28.0": + version: 4.28.1 + resolution: "browserslist@npm:4.28.1" dependencies: - baseline-browser-mapping: "npm:^2.8.3" - caniuse-lite: "npm:^1.0.30001741" - electron-to-chromium: "npm:^1.5.218" - node-releases: "npm:^2.0.21" - update-browserslist-db: "npm:^1.1.3" + baseline-browser-mapping: "npm:^2.9.0" + caniuse-lite: "npm:^1.0.30001759" + electron-to-chromium: "npm:^1.5.263" + node-releases: "npm:^2.0.27" + update-browserslist-db: "npm:^1.2.0" bin: browserslist: cli.js - checksum: 10c0/1146339dad33fda77786b11ea07f1c40c48899edd897d73a9114ee0dbb1ee6475bb4abda263a678c104508bdca8e66760ff8e10be1947d3e20d34bae01d8b89b + checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd languageName: node linkType: hard @@ -5753,10 +5774,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001741": - version: 1.0.30001743 - resolution: "caniuse-lite@npm:1.0.30001743" - checksum: 10c0/1bd730ca10d881a1ca9f55ce864d34c3b18501718c03976e0d3419f4694b715159e13fdef6d58ad47b6d2445d315940f3a01266658876828c820a3331aac021d +"caniuse-lite@npm:^1.0.30001754, caniuse-lite@npm:^1.0.30001759": + version: 1.0.30001759 + resolution: "caniuse-lite@npm:1.0.30001759" + checksum: 10c0/b0f415960ba34995cda18e0d25c4e602f6917b9179290a76bdd0311423505b78cc93e558a90c98a22a1cc6b1781ab720ef6beea24ec7e29a1c1164ca72eac3a2 languageName: node linkType: hard @@ -6216,10 +6237,10 @@ __metadata: languageName: node linkType: hard -"cssdb@npm:^8.4.2": - version: 8.4.2 - resolution: "cssdb@npm:8.4.2" - checksum: 10c0/3c88610ba9e3f87f9ecf068b72261e90de8bb1f5d1dceefc79ff42b2e19f5814135937ad057b7f8c4bf58212f911e5f9d2f6f0910af3da127170009f1f75689c +"cssdb@npm:^8.5.2": + version: 8.5.2 + resolution: "cssdb@npm:8.5.2" + checksum: 10c0/12f7ed29dda0d74b209d6470acd246b335aac507c2786c17f20709f856eabb24e6d43ff44507898f5a1b0a101b286b997d95682e44b06f4c7cb4bd7081db7c32 languageName: node linkType: hard @@ -6551,10 +6572,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.218": - version: 1.5.222 - resolution: "electron-to-chromium@npm:1.5.222" - checksum: 10c0/a81eb8d2b171236884faf9b5dd382c66d9250283032cb89a3e555d788bf3956f7f4f6bf7bf30b3daf9e5c945ef837bfcd1be21b3f41cfe186ed2f25da13c9af3 +"electron-to-chromium@npm:^1.5.263": + version: 1.5.266 + resolution: "electron-to-chromium@npm:1.5.266" + checksum: 10c0/74ada92ada1ace76ec5b7da8a9cc2d7f03db122a64ac8e12ae30eba3e358ffec443c0c5265bc6edcdeebfa73f449b21c361080c064eb1eec437db2d71fc03248 languageName: node linkType: hard @@ -7652,10 +7673,10 @@ __metadata: languageName: node linkType: hard -"fraction.js@npm:^4.3.7": - version: 4.3.7 - resolution: "fraction.js@npm:4.3.7" - checksum: 10c0/df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711 +"fraction.js@npm:^5.3.4": + version: 5.3.4 + resolution: "fraction.js@npm:5.3.4" + checksum: 10c0/f90079fe9bfc665e0a07079938e8ff71115bce9462f17b32fc283f163b0540ec34dc33df8ed41bb56f028316b04361b9a9995b9ee9258617f8338e0b05c5f95a languageName: node linkType: hard @@ -9908,10 +9929,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.21": - version: 2.0.21 - resolution: "node-releases@npm:2.0.21" - checksum: 10c0/0eb94916eeebbda9d51da6a9ea47428a12b2bb0dd94930c949632b0c859356abf53b2e5a2792021f96c5fda4f791a8e195f2375b78ae7dba8d8bc3141baa1469 +"node-releases@npm:^2.0.27": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2 languageName: node linkType: hard @@ -10870,8 +10891,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.1.5": - version: 10.4.0 - resolution: "postcss-preset-env@npm:10.4.0" + version: 10.5.0 + resolution: "postcss-preset-env@npm:10.5.0" dependencies: "@csstools/postcss-alpha-function": "npm:^1.0.1" "@csstools/postcss-cascade-layers": "npm:^5.0.2" @@ -10900,21 +10921,23 @@ __metadata: "@csstools/postcss-nested-calc": "npm:^4.0.0" "@csstools/postcss-normalize-display-values": "npm:^4.0.0" "@csstools/postcss-oklab-function": "npm:^4.0.12" + "@csstools/postcss-position-area-property": "npm:^1.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/postcss-random-function": "npm:^2.0.1" "@csstools/postcss-relative-color-syntax": "npm:^3.0.12" "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" "@csstools/postcss-sign-functions": "npm:^1.1.4" "@csstools/postcss-stepped-value-functions": "npm:^4.0.9" + "@csstools/postcss-system-ui-font-family": "npm:^1.0.0" "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.3" "@csstools/postcss-trigonometric-functions": "npm:^4.0.9" "@csstools/postcss-unset-value": "npm:^4.0.0" - autoprefixer: "npm:^10.4.21" - browserslist: "npm:^4.26.0" + autoprefixer: "npm:^10.4.22" + browserslist: "npm:^4.28.0" css-blank-pseudo: "npm:^7.0.1" css-has-pseudo: "npm:^7.0.3" css-prefers-color-scheme: "npm:^10.0.0" - cssdb: "npm:^8.4.2" + cssdb: "npm:^8.5.2" postcss-attribute-case-insensitive: "npm:^7.0.1" postcss-clamp: "npm:^4.1.0" postcss-color-functional-notation: "npm:^7.0.12" @@ -10942,7 +10965,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/3c081a66ebde19ae2f915f4eb103b85097085799b43103e5dd1699ed807bd54c80d633c7d4b525badaf21e9d0b217e6ca169ee306e2b720bb70b7414ad375387 + checksum: 10c0/4e9881478b465e8eb7493c1240cb2df8523944135728672e8feeb8bb3f6a48b00d67d007ee8fbdcee648ab9ebdfca10a7591f42e3c6b9076cbf7f355f8ad1574 languageName: node linkType: hard @@ -13880,9 +13903,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.3": - version: 1.1.3 - resolution: "update-browserslist-db@npm:1.1.3" +"update-browserslist-db@npm:^1.2.0": + version: 1.2.2 + resolution: "update-browserslist-db@npm:1.2.2" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -13890,7 +13913,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10c0/682e8ecbf9de474a626f6462aa85927936cdd256fe584c6df2508b0df9f7362c44c957e9970df55dfe44d3623807d26316ea2c7d26b80bb76a16c56c37233c32 + checksum: 10c0/39c3ea08b397ffc8dc3a1c517f5c6ed5cc4179b5e185383dab9bf745879623c12062a2e6bf4f9427cc59389c7bfa0010e86858b923c1e349e32fdddd9b043bb2 languageName: node linkType: hard