From 8c7642cd186315f364a3a2d9090ebd87c2d684c6 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Wed, 21 Aug 2024 17:56:36 +0900 Subject: [PATCH 1/7] Change translation strings of grouped notification label to have full context (#31486) --- .../components/displayed_name.tsx | 22 ++++++++ .../components/names_list.tsx | 51 ------------------- .../components/notification_admin_sign_up.tsx | 28 +++++++--- .../components/notification_favourite.tsx | 32 +++++++++--- .../components/notification_follow.tsx | 28 +++++++--- .../notification_follow_request.tsx | 28 +++++++--- .../notification_group_with_status.tsx | 20 ++++---- .../components/notification_reblog.tsx | 32 +++++++++--- .../components/notification_status.tsx | 4 +- .../components/notification_update.tsx | 4 +- .../components/notification_with_status.tsx | 7 +-- app/javascript/mastodon/locales/en.json | 7 ++- 12 files changed, 155 insertions(+), 108 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications_v2/components/displayed_name.tsx delete mode 100644 app/javascript/mastodon/features/notifications_v2/components/names_list.tsx diff --git a/app/javascript/mastodon/features/notifications_v2/components/displayed_name.tsx b/app/javascript/mastodon/features/notifications_v2/components/displayed_name.tsx new file mode 100644 index 00000000000000..82ecb93ee5fdb9 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/displayed_name.tsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; + +import { useAppSelector } from 'mastodon/store'; + +export const DisplayedName: React.FC<{ + accountIds: string[]; +}> = ({ accountIds }) => { + const lastAccountId = accountIds[0] ?? '0'; + const account = useAppSelector((state) => state.accounts.get(lastAccountId)); + + if (!account) return null; + + return ( + + + + ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/components/names_list.tsx b/app/javascript/mastodon/features/notifications_v2/components/names_list.tsx deleted file mode 100644 index 3d70cc0b623260..00000000000000 --- a/app/javascript/mastodon/features/notifications_v2/components/names_list.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useAppSelector } from 'mastodon/store'; - -export const NamesList: React.FC<{ - accountIds: string[]; - total: number; - seeMoreHref?: string; -}> = ({ accountIds, total, seeMoreHref }) => { - const lastAccountId = accountIds[0] ?? '0'; - const account = useAppSelector((state) => state.accounts.get(lastAccountId)); - - if (!account) return null; - - const displayedName = ( - - - - ); - - if (total === 1) { - return displayedName; - } - - if (seeMoreHref) - return ( - {chunks}, - }} - /> - ); - - return ( - - ); -}; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_admin_sign_up.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_sign_up.tsx index 9f7afc63f5b073..73a5851ad3c833 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_admin_sign_up.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_sign_up.tsx @@ -6,13 +6,27 @@ import type { NotificationGroupAdminSignUp } from 'mastodon/models/notification_ import type { LabelRenderer } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status'; -const labelRenderer: LabelRenderer = (values) => ( - -); +const labelRenderer: LabelRenderer = (displayedName, total) => { + if (total === 1) + return ( + + ); + + return ( + + ); +}; export const NotificationAdminSignUp: React.FC<{ notification: NotificationGroupAdminSignUp; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx index 22838fe69bc0be..eba37fe9371c44 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx @@ -1,5 +1,7 @@ import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import type { NotificationGroupFavourite } from 'mastodon/models/notification_group'; import { useAppSelector } from 'mastodon/store'; @@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store'; import type { LabelRenderer } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status'; -const labelRenderer: LabelRenderer = (values) => ( - -); +const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => { + if (total === 1) + return ( + + ); + + return ( + + seeMoreHref ? {chunks} : chunks, + }} + /> + ); +}; export const NotificationFavourite: React.FC<{ notification: NotificationGroupFavourite; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx index 3760096d4427e9..6a9a45d242c348 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx @@ -10,13 +10,27 @@ import { useAppSelector } from 'mastodon/store'; import type { LabelRenderer } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status'; -const labelRenderer: LabelRenderer = (values) => ( - -); +const labelRenderer: LabelRenderer = (displayedName, total) => { + if (total === 1) + return ( + + ); + + return ( + + ); +}; const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => { const account = useAppSelector((s) => s.accounts.get(accountId)); diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_follow_request.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_follow_request.tsx index 281bfd94af67b9..5f61f2cd780afe 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_follow_request.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_follow_request.tsx @@ -21,13 +21,27 @@ const messages = defineMessages({ reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, }); -const labelRenderer: LabelRenderer = (values) => ( - -); +const labelRenderer: LabelRenderer = (displayedName, total) => { + if (total === 1) + return ( + + ); + + return ( + + ); +}; export const NotificationFollowRequest: React.FC<{ notification: NotificationGroupFollowRequest; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx index 8f555956e0e771..343a9bc4c1ae2e 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx @@ -12,11 +12,13 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { useAppDispatch } from 'mastodon/store'; import { AvatarGroup } from './avatar_group'; +import { DisplayedName } from './displayed_name'; import { EmbeddedStatus } from './embedded_status'; -import { NamesList } from './names_list'; export type LabelRenderer = ( - values: Record, + displayedName: JSX.Element, + total: number, + seeMoreHref?: string, ) => JSX.Element; export const NotificationGroupWithStatus: React.FC<{ @@ -50,15 +52,11 @@ export const NotificationGroupWithStatus: React.FC<{ const label = useMemo( () => - labelRenderer({ - name: ( - - ), - }), + labelRenderer( + , + count, + labelSeeMoreHref, + ), [labelRenderer, accountIds, count, labelSeeMoreHref], ); diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_reblog.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_reblog.tsx index 06255686884142..7b3bda85e57e46 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_reblog.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_reblog.tsx @@ -1,5 +1,7 @@ import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import type { NotificationGroupReblog } from 'mastodon/models/notification_group'; import { useAppSelector } from 'mastodon/store'; @@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store'; import type { LabelRenderer } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status'; -const labelRenderer: LabelRenderer = (values) => ( - -); +const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => { + if (total === 1) + return ( + + ); + + return ( + + seeMoreHref ? {chunks} : chunks, + }} + /> + ); +}; export const NotificationReblog: React.FC<{ notification: NotificationGroupReblog; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_status.tsx index 9ade355a71494b..2955c3aeac5f1b 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_status.tsx @@ -6,11 +6,11 @@ import type { NotificationGroupStatus } from 'mastodon/models/notification_group import type { LabelRenderer } from './notification_group_with_status'; import { NotificationWithStatus } from './notification_with_status'; -const labelRenderer: LabelRenderer = (values) => ( +const labelRenderer: LabelRenderer = (displayedName) => ( ); diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_update.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_update.tsx index c518367bf5a24f..731f319f894a5c 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_update.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_update.tsx @@ -6,11 +6,11 @@ import type { NotificationGroupUpdate } from 'mastodon/models/notification_group import type { LabelRenderer } from './notification_group_with_status'; import { NotificationWithStatus } from './notification_with_status'; -const labelRenderer: LabelRenderer = (values) => ( +const labelRenderer: LabelRenderer = (displayedName) => ( ); diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx index 5d5cb98185bdf2..a1c275a1f39309 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx @@ -15,7 +15,7 @@ import { Icon } from 'mastodon/components/icon'; import Status from 'mastodon/containers/status_container'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; -import { NamesList } from './names_list'; +import { DisplayedName } from './displayed_name'; import type { LabelRenderer } from './notification_group_with_status'; export const NotificationWithStatus: React.FC<{ @@ -40,10 +40,7 @@ export const NotificationWithStatus: React.FC<{ const dispatch = useAppDispatch(); const label = useMemo( - () => - labelRenderer({ - name: , - }), + () => labelRenderer(, count), [labelRenderer, accountIds, count], ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 43ff015791fbf0..ba351032ffd2a0 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -463,8 +463,6 @@ "mute_modal.title": "Mute user?", "mute_modal.you_wont_see_mentions": "You won't see posts that mention them.", "mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.", - "name_and_others": "{name} and {count, plural, one {# other} other {# others}}", - "name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}}", "navigation_bar.about": "About", "navigation_bar.advanced_interface": "Open in advanced web interface", "navigation_bar.blocks": "Blocked users", @@ -497,9 +495,13 @@ "notification.admin.report_statuses": "{name} reported {target} for {category}", "notification.admin.report_statuses_other": "{name} reported {target}", "notification.admin.sign_up": "{name} signed up", + "notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up", "notification.favourite": "{name} favorited your post", + "notification.favourite.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} favorited your post", "notification.follow": "{name} followed you", + "notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you", "notification.follow_request": "{name} has requested to follow you", + "notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you", "notification.label.mention": "Mention", "notification.label.private_mention": "Private mention", "notification.label.private_reply": "Private reply", @@ -517,6 +519,7 @@ "notification.own_poll": "Your poll has ended", "notification.poll": "A poll you voted in has ended", "notification.reblog": "{name} boosted your post", + "notification.reblog.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} boosted your post", "notification.relationships_severance_event": "Lost connections with {name}", "notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.", "notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.", From e48a64d3b541087acd101f9149c956ffb07119f3 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Aug 2024 12:25:14 +0200 Subject: [PATCH 2/7] Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524) --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b6e71cde41bfb7..3d280ac4afd312 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -6247,7 +6247,7 @@ a.status-card { } .boost-modal__container { - overflow-x: scroll; + overflow-y: auto; padding: 10px; .status { From 01a757d306276213276580a31ecbeffa21d88076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?= Date: Wed, 21 Aug 2024 22:11:36 +0900 Subject: [PATCH 3/7] Fix boost dialog visibility selection not being taken into account (#31523) --- app/javascript/mastodon/actions/interactions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index b296a5006ac55c..d3538a8850a3d7 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -437,12 +437,12 @@ export function unpinFail(status, error) { }; } -function toggleReblogWithoutConfirmation(status, privacy) { +function toggleReblogWithoutConfirmation(status, visibility) { return (dispatch) => { if (status.get('reblogged')) { dispatch(unreblog({ statusId: status.get('id') })); } else { - dispatch(reblog({ statusId: status.get('id'), privacy })); + dispatch(reblog({ statusId: status.get('id'), visibility })); } }; } From d67e11733e4159c736f4370ee7ea86240a297bb7 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Aug 2024 16:41:31 +0200 Subject: [PATCH 4/7] Add automatic notification polling for grouped notifications (#31513) --- .../mastodon/actions/notification_groups.ts | 23 ++ app/javascript/mastodon/actions/streaming.js | 25 +- app/javascript/mastodon/api/notifications.ts | 1 + .../mastodon/reducers/notification_groups.ts | 222 +++++++++++------- 4 files changed, 186 insertions(+), 85 deletions(-) diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 6b699706e2536a..51f83f1d241a4a 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -11,6 +11,7 @@ import type { } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications'; import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; +import { usePendingItems } from 'mastodon/initial_state'; import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import { selectSettingsNotificationsExcludedTypes, @@ -103,6 +104,28 @@ export const fetchNotificationsGap = createDataLoadingThunk( }, ); +export const pollRecentNotifications = createDataLoadingThunk( + 'notificationGroups/pollRecentNotifications', + async (_params, { getState }) => { + return apiFetchNotifications({ + max_id: undefined, + // In slow mode, we don't want to include notifications that duplicate the already-displayed ones + since_id: usePendingItems + ? getState().notificationGroups.groups.find( + (group) => group.type !== 'gap', + )?.page_max_id + : undefined, + }); + }, + ({ notifications, accounts, statuses }, { dispatch }) => { + dispatch(importFetchedAccounts(accounts)); + dispatch(importFetchedStatuses(statuses)); + dispatchAssociatedRecords(dispatch, notifications); + + return { notifications }; + }, +); + export const processNewNotificationForGroups = createAppAsyncThunk( 'notificationGroups/processNew', (notification: ApiNotificationJSON, { dispatch, getState }) => { diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index e082aea43d9116..04f5e6b88c5b90 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -10,7 +10,7 @@ import { deleteAnnouncement, } from './announcements'; import { updateConversations } from './conversations'; -import { processNewNotificationForGroups, refreshStaleNotificationGroups } from './notification_groups'; +import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; import { updateNotifications, expandNotifications } from './notifications'; import { updateStatus } from './statuses'; import { @@ -37,7 +37,7 @@ const randomUpTo = max => * @param {string} channelName * @param {Object.} params * @param {Object} options - * @param {function(Function): Promise} [options.fallback] + * @param {function(Function, Function): Promise} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] * @returns {function(): void} @@ -52,11 +52,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti let pollingId; /** - * @param {function(Function): Promise} fallback + * @param {function(Function, Function): Promise} fallback */ const useFallback = async fallback => { - await fallback(dispatch); + await fallback(dispatch, getState); // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }; @@ -139,10 +139,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti /** * @param {Function} dispatch + * @param {Function} getState */ -async function refreshHomeTimelineAndNotification(dispatch) { +async function refreshHomeTimelineAndNotification(dispatch, getState) { await dispatch(expandHomeTimeline({ maxId: undefined })); - await dispatch(expandNotifications({})); + + // TODO: remove this once the groups feature replaces the previous one + if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) { + // TODO: polling for merged notifications + try { + await dispatch(pollRecentGroupNotifications()); + } catch (error) { + // TODO + } + } else { + await dispatch(expandNotifications({})); + } + await dispatch(fetchAnnouncements()); } diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts index ed187da5eccae0..cb07e4114cf8b5 100644 --- a/app/javascript/mastodon/api/notifications.ts +++ b/app/javascript/mastodon/api/notifications.ts @@ -4,6 +4,7 @@ import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notific export const apiFetchNotifications = async (params?: { exclude_types?: string[]; max_id?: string; + since_id?: string; }) => { const response = await api().request({ method: 'GET', diff --git a/app/javascript/mastodon/reducers/notification_groups.ts b/app/javascript/mastodon/reducers/notification_groups.ts index 0348f080ff2e76..b3535d7b672017 100644 --- a/app/javascript/mastodon/reducers/notification_groups.ts +++ b/app/javascript/mastodon/reducers/notification_groups.ts @@ -20,12 +20,16 @@ import { mountNotifications, unmountNotifications, refreshStaleNotificationGroups, + pollRecentNotifications, } from 'mastodon/actions/notification_groups'; import { disconnectTimeline, timelineDelete, } from 'mastodon/actions/timelines_typed'; -import type { ApiNotificationJSON } from 'mastodon/api_types/notifications'; +import type { + ApiNotificationJSON, + ApiNotificationGroupJSON, +} from 'mastodon/api_types/notifications'; import { compareId } from 'mastodon/compare_id'; import { usePendingItems } from 'mastodon/initial_state'; import { @@ -296,6 +300,106 @@ function commitLastReadId(state: NotificationGroupsState) { } } +function fillNotificationsGap( + groups: NotificationGroupsState['groups'], + gap: NotificationGap, + notifications: ApiNotificationGroupJSON[], +): NotificationGroupsState['groups'] { + // find the gap in the existing notifications + const gapIndex = groups.findIndex( + (groupOrGap) => + groupOrGap.type === 'gap' && + groupOrGap.sinceId === gap.sinceId && + groupOrGap.maxId === gap.maxId, + ); + + if (gapIndex < 0) + // We do not know where to insert, let's return + return groups; + + // Filling a disconnection gap means we're getting historical data + // about groups we may know or may not know about. + + // The notifications timeline is split in two by the gap, with + // group information newer than the gap, and group information older + // than the gap. + + // Filling a gap should not touch anything before the gap, so any + // information on groups already appearing before the gap should be + // discarded, while any information on groups appearing after the gap + // can be updated and re-ordered. + + const oldestPageNotification = notifications.at(-1)?.page_min_id; + + // replace the gap with the notifications + a new gap + + const newerGroupKeys = groups + .slice(0, gapIndex) + .filter(isNotificationGroup) + .map((group) => group.group_key); + + const toInsert: NotificationGroupsState['groups'] = notifications + .map((json) => createNotificationGroupFromJSON(json)) + .filter((notification) => !newerGroupKeys.includes(notification.group_key)); + + const apiGroupKeys = (toInsert as NotificationGroup[]).map( + (group) => group.group_key, + ); + + const sinceId = gap.sinceId; + if ( + notifications.length > 0 && + !( + oldestPageNotification && + sinceId && + compareId(oldestPageNotification, sinceId) <= 0 + ) + ) { + // If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap + // Similarly, if we've fetched more than the gap's, this means we have completely filled it + toInsert.push({ + type: 'gap', + maxId: notifications.at(-1)?.page_max_id, + sinceId, + } as NotificationGap); + } + + // Remove older groups covered by the API + groups = groups.filter( + (groupOrGap) => + groupOrGap.type !== 'gap' && !apiGroupKeys.includes(groupOrGap.group_key), + ); + + // Replace the gap with API results (+ the new gap if needed) + groups.splice(gapIndex, 1, ...toInsert); + + // Finally, merge any adjacent gaps that could have been created by filtering + // groups earlier + mergeGaps(groups); + + return groups; +} + +// Ensure the groups list starts with a gap, mutating it to prepend one if needed +function ensureLeadingGap( + groups: NotificationGroupsState['groups'], +): NotificationGap { + if (groups[0]?.type === 'gap') { + // We're expecting new notifications, so discard the maxId if there is one + groups[0].maxId = undefined; + + return groups[0]; + } else { + const gap: NotificationGap = { + type: 'gap', + sinceId: groups[0]?.page_min_id, + }; + + groups.unshift(gap); + return gap; + } +} + export const notificationGroupsReducer = createReducer( initialState, (builder) => { @@ -309,86 +413,36 @@ export const notificationGroupsReducer = createReducer( updateLastReadId(state); }) .addCase(fetchNotificationsGap.fulfilled, (state, action) => { - const { notifications } = action.payload; - - // find the gap in the existing notifications - const gapIndex = state.groups.findIndex( - (groupOrGap) => - groupOrGap.type === 'gap' && - groupOrGap.sinceId === action.meta.arg.gap.sinceId && - groupOrGap.maxId === action.meta.arg.gap.maxId, + state.groups = fillNotificationsGap( + state.groups, + action.meta.arg.gap, + action.payload.notifications, ); + state.isLoading = false; - if (gapIndex < 0) - // We do not know where to insert, let's return - return; - - // Filling a disconnection gap means we're getting historical data - // about groups we may know or may not know about. - - // The notifications timeline is split in two by the gap, with - // group information newer than the gap, and group information older - // than the gap. - - // Filling a gap should not touch anything before the gap, so any - // information on groups already appearing before the gap should be - // discarded, while any information on groups appearing after the gap - // can be updated and re-ordered. - - const oldestPageNotification = notifications.at(-1)?.page_min_id; - - // replace the gap with the notifications + a new gap - - const newerGroupKeys = state.groups - .slice(0, gapIndex) - .filter(isNotificationGroup) - .map((group) => group.group_key); - - const toInsert: NotificationGroupsState['groups'] = notifications - .map((json) => createNotificationGroupFromJSON(json)) - .filter( - (notification) => !newerGroupKeys.includes(notification.group_key), + updateLastReadId(state); + }) + .addCase(pollRecentNotifications.fulfilled, (state, action) => { + if (usePendingItems) { + const gap = ensureLeadingGap(state.pendingGroups); + state.pendingGroups = fillNotificationsGap( + state.pendingGroups, + gap, + action.payload.notifications, + ); + } else { + const gap = ensureLeadingGap(state.groups); + state.groups = fillNotificationsGap( + state.groups, + gap, + action.payload.notifications, ); - - const apiGroupKeys = (toInsert as NotificationGroup[]).map( - (group) => group.group_key, - ); - - const sinceId = action.meta.arg.gap.sinceId; - if ( - notifications.length > 0 && - !( - oldestPageNotification && - sinceId && - compareId(oldestPageNotification, sinceId) <= 0 - ) - ) { - // If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap - // Similarly, if we've fetched more than the gap's, this means we have completely filled it - toInsert.push({ - type: 'gap', - maxId: notifications.at(-1)?.page_max_id, - sinceId, - } as NotificationGap); } - // Remove older groups covered by the API - state.groups = state.groups.filter( - (groupOrGap) => - groupOrGap.type !== 'gap' && - !apiGroupKeys.includes(groupOrGap.group_key), - ); - - // Replace the gap with API results (+ the new gap if needed) - state.groups.splice(gapIndex, 1, ...toInsert); - - // Finally, merge any adjacent gaps that could have been created by filtering - // groups earlier - mergeGaps(state.groups); - state.isLoading = false; updateLastReadId(state); + trimNotifications(state); }) .addCase(processNewNotificationForGroups.fulfilled, (state, action) => { const notification = action.payload; @@ -403,10 +457,11 @@ export const notificationGroupsReducer = createReducer( }) .addCase(disconnectTimeline, (state, action) => { if (action.payload.timeline === 'home') { - if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') { - state.groups.unshift({ + const groups = usePendingItems ? state.pendingGroups : state.groups; + if (groups.length > 0 && groups[0]?.type !== 'gap') { + groups.unshift({ type: 'gap', - sinceId: state.groups[0]?.page_min_id, + sinceId: groups[0]?.page_min_id, }); } } @@ -453,12 +508,13 @@ export const notificationGroupsReducer = createReducer( } } } - trimNotifications(state); }); // Then build the consolidated list and clear pending groups state.groups = state.pendingGroups.concat(state.groups); state.pendingGroups = []; + mergeGaps(state.groups); + trimNotifications(state); }) .addCase(updateScrollPosition.fulfilled, (state, action) => { state.scrolledToTop = action.payload.top; @@ -518,13 +574,21 @@ export const notificationGroupsReducer = createReducer( }, ) .addMatcher( - isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending), + isAnyOf( + fetchNotifications.pending, + fetchNotificationsGap.pending, + pollRecentNotifications.pending, + ), (state) => { state.isLoading = true; }, ) .addMatcher( - isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected), + isAnyOf( + fetchNotifications.rejected, + fetchNotificationsGap.rejected, + pollRecentNotifications.rejected, + ), (state) => { state.isLoading = false; }, From 19a1acb38b66c9d9f6e1ca937c9f163c4ffe9194 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Aug 2024 16:54:59 +0200 Subject: [PATCH 5/7] Add `api_versions` to `/api/v2/instance` (#31354) --- app/serializers/rest/instance_serializer.rb | 8 +++++++- spec/requests/api/v2/instance_spec.rb | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 8df79db6c774fd..5475bd12b43f2f 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -11,7 +11,7 @@ class ContactSerializer < ActiveModel::Serializer attributes :domain, :title, :version, :source_url, :description, :usage, :thumbnail, :languages, :configuration, - :registrations + :registrations, :api_versions has_one :contact, serializer: ContactSerializer has_many :rules, serializer: REST::RuleSerializer @@ -94,6 +94,12 @@ def registrations } end + def api_versions + { + mastodon: 1, + } + end + private def registrations_enabled? diff --git a/spec/requests/api/v2/instance_spec.rb b/spec/requests/api/v2/instance_spec.rb index 5c464f09a7d06e..2636970d6e5a36 100644 --- a/spec/requests/api/v2/instance_spec.rb +++ b/spec/requests/api/v2/instance_spec.rb @@ -18,6 +18,7 @@ expect(body_as_json) .to be_present .and include(title: 'Mastodon') + .and include_api_versions .and include_configuration_limits end end @@ -32,6 +33,7 @@ expect(body_as_json) .to be_present .and include(title: 'Mastodon') + .and include_api_versions .and include_configuration_limits end end @@ -53,5 +55,13 @@ def include_configuration_limits ) ) end + + def include_api_versions + include( + api_versions: include( + mastodon: anything + ) + ) + end end end From edeae945c0c9d6318d489ab720587621545063d0 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Aug 2024 17:55:35 +0200 Subject: [PATCH 6/7] Remove fontawesome leftovers (#31525) --- app/javascript/styles/mastodon/accounts.scss | 10 ---------- app/javascript/styles/mastodon/admin.scss | 8 -------- app/javascript/styles/mastodon/components.scss | 4 ---- app/javascript/styles/mastodon/dashboard.scss | 4 ---- app/javascript/styles/mastodon/forms.scss | 4 ---- app/javascript/styles/mastodon/rtl.scss | 8 -------- app/javascript/styles/mastodon/tables.scss | 5 ----- app/javascript/styles/mastodon/widgets.scss | 9 +-------- app/views/statuses/_poll.html.haml | 6 +----- 9 files changed, 2 insertions(+), 56 deletions(-) diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 3f70a7d234327c..c769c88f75ae5a 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -130,21 +130,11 @@ .older { float: left; padding-inline-start: 0; - - .fa { - display: inline-block; - margin-inline-end: 5px; - } } .newer { float: right; padding-inline-end: 0; - - .fa { - display: inline-block; - margin-inline-start: 5px; - } } .disabled { diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 0df8d7a271cb01..fd9515cfefa62e 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -122,10 +122,6 @@ $content-width: 840px; overflow: hidden; text-overflow: ellipsis; - i.fa { - margin-inline-end: 5px; - } - &:hover { color: $primary-text-color; transition: all 100ms linear; @@ -306,10 +302,6 @@ $content-width: 840px; box-shadow: none; } - .directory__tag .table-action-link .fa { - color: inherit; - } - .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 3d280ac4afd312..761dfb5696ca73 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2891,10 +2891,6 @@ $ui-header-logo-wordmark-width: 99px; padding-inline-end: 30px; } - .search__icon .fa { - top: 15px; - } - .scrollable { overflow: visible; diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index 12d0a6b92f9b90..1621220ccb522e 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -113,10 +113,6 @@ flex: 1 1 auto; } - .fa { - flex: 0 0 auto; - } - strong { font-weight: 700; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index cf8c1327dc51be..926df4e96fc039 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -930,10 +930,6 @@ code { font-weight: 700; } } - - .fa { - font-weight: 400; - } } } } diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index 07fe96fc3ad1f1..e4e299ff82d9c2 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -41,14 +41,6 @@ body.rtl { no-repeat left 8px center / auto 16px; } - .fa-chevron-left::before { - content: '\F054'; - } - - .fa-chevron-right::before { - content: '\F053'; - } - .dismissable-banner, .warning-banner { &__action { diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 4997ed9b847611..2becd85bc667ef 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -142,11 +142,6 @@ a.table-action-link { color: $highlight-text-color; } - i.fa { - font-weight: 400; - margin-inline-end: 5px; - } - &:first-child { padding-inline-start: 0; } diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index da7d68ce8db39d..a8f678d482a2fe 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -169,11 +169,6 @@ &__message { margin-bottom: 15px; - - .fa { - margin-inline-end: 5px; - color: $darker-text-color; - } } &__card { @@ -366,9 +361,7 @@ padding-inline-end: 16px; } - .fa { - font-size: 16px; - + .icon { &.active { color: $highlight-text-color; } diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml index cf01b1da018dd6..62416a44da6dfd 100644 --- a/app/views/statuses/_poll.html.haml +++ b/app/views/statuses/_poll.html.haml @@ -1,11 +1,10 @@ :ruby show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? - own_votes = user_signed_in? ? poll.own_votes(current_account) : [] total_votes_count = poll.voters_count || poll.votes_count .poll %ul - - poll.loaded_options.each_with_index do |option, index| + - poll.loaded_options.each do |option| %li - if show_results - percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0 @@ -14,9 +13,6 @@ #{percent.round}% %span.poll__option__text = prerender_custom_emojis(h(option.title), status.emojis) - - if own_votes.include?(index) - %span.poll__voted - %i.poll__voted__mark.fa.fa-check %progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' } %span.poll__chart From 2da687a28b509025343d3d8ca17753de9b128e8f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Aug 2024 19:05:02 +0200 Subject: [PATCH 7/7] Remove dead CSS code (#31527) --- .../styles/mastodon-light/diff.scss | 3 - .../styles/mastodon/components.scss | 20 -- app/javascript/styles/mastodon/widgets.scss | 310 ------------------ 3 files changed, 333 deletions(-) diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 8d801e4cd5b854..1df556b42a4e97 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -414,9 +414,6 @@ border-color: transparent transparent $white; } -.hero-widget, -.moved-account-widget, -.memoriam-widget, .activity-stream, .nothing-here, .directory__tag > a, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 761dfb5696ca73..3d2c4662541985 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3422,26 +3422,6 @@ $ui-header-logo-wordmark-width: 99px; height: calc(100% - 10px); overflow-y: hidden; - .hero-widget { - box-shadow: none; - - &__text, - &__img, - &__img img { - border-radius: 0; - } - - &__text { - padding: 15px; - color: $secondary-text-color; - - strong { - font-weight: 700; - color: $primary-text-color; - } - } - } - .compose-form { flex: 1 1 auto; min-height: 0; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index a8f678d482a2fe..b37d790ce39262 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -1,289 +1,3 @@ -@use 'sass:math'; - -.hero-widget { - margin-bottom: 10px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - &:last-child { - margin-bottom: 0; - } - - &__img { - width: 100%; - position: relative; - overflow: hidden; - border-radius: 4px 4px 0 0; - background: $base-shadow-color; - - img { - object-fit: cover; - display: block; - width: 100%; - height: 100%; - margin: 0; - border-radius: 4px 4px 0 0; - } - } - - &__text { - background: $ui-base-color; - padding: 20px; - border-radius: 0 0 4px 4px; - font-size: 15px; - color: $darker-text-color; - line-height: 20px; - word-wrap: break-word; - font-weight: 400; - - .emojione { - width: 20px; - height: 20px; - margin: -3px 0 0; - margin-inline-start: 0.075em; - margin-inline-end: 0.075em; - } - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - em { - display: inline; - margin: 0; - padding: 0; - font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; - color: lighten($darker-text-color, 10%); - } - - a { - color: $secondary-text-color; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - display: none; - } -} - -.endorsements-widget { - margin-bottom: 10px; - padding-bottom: 10px; - - h4 { - padding: 10px; - text-transform: uppercase; - font-weight: 700; - font-size: 13px; - color: $darker-text-color; - } - - .account { - padding: 10px 0; - - &:last-child { - border-bottom: 0; - } - - .account__display-name { - display: flex; - align-items: center; - } - } - - .trends__item { - padding: 10px; - } -} - -.trends-widget { - h4 { - color: $darker-text-color; - } -} - -.placeholder-widget { - padding: 16px; - border-radius: 4px; - border: 2px dashed $dark-text-color; - text-align: center; - color: $darker-text-color; - margin-bottom: 10px; -} - -.moved-account-widget { - padding: 15px; - padding-bottom: 20px; - border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - color: $secondary-text-color; - font-weight: 400; - margin-bottom: 10px; - - strong, - a { - font-weight: 500; - - @each $lang in $cjk-langs { - &:lang(#{$lang}) { - font-weight: 700; - } - } - } - - a { - color: inherit; - text-decoration: underline; - - &.mention { - text-decoration: none; - - span { - text-decoration: none; - } - - &:focus, - &:hover, - &:active { - text-decoration: none; - - span { - text-decoration: underline; - } - } - } - } - - &__message { - margin-bottom: 15px; - } - - &__card { - .detailed-status__display-avatar { - position: relative; - cursor: pointer; - } - - .detailed-status__display-name { - margin-bottom: 0; - text-decoration: none; - - span { - font-weight: 400; - } - } - } -} - -.memoriam-widget { - padding: 20px; - border-radius: 4px; - background: $base-shadow-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - font-size: 14px; - color: $darker-text-color; - margin-bottom: 10px; -} - -.directory { - background: var(--background-color); - border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - &__tag { - box-sizing: border-box; - margin-bottom: 10px; - - & > a, - & > div { - display: flex; - align-items: center; - justify-content: space-between; - border: 1px solid var(--background-border-color); - border-radius: 4px; - padding: 15px; - text-decoration: none; - color: inherit; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - } - - & > a { - &:hover, - &:active, - &:focus { - background: $ui-base-color; - } - } - - &.active > a { - background: $ui-highlight-color; - cursor: default; - } - - &.disabled > div { - opacity: 0.5; - cursor: default; - } - - h4 { - flex: 1 1 auto; - font-size: 18px; - font-weight: 700; - color: $primary-text-color; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - .fa { - color: $darker-text-color; - } - - small { - display: block; - font-weight: 400; - font-size: 15px; - margin-top: 8px; - color: $darker-text-color; - } - } - - &.active h4 { - &, - .fa, - small, - .trends__item__current { - color: $primary-text-color; - } - } - - .avatar-stack { - flex: 0 0 auto; - width: (36px + 4px) * 3; - } - - &.active .avatar-stack .account__avatar { - border-color: $ui-highlight-color; - } - - .trends__item__current { - padding-inline-end: 0; - } - } -} - .accounts-table { width: 100%; @@ -381,27 +95,3 @@ } } } - -.moved-account-widget, -.memoriam-widget, -.directory { - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - box-shadow: none; - border-radius: 0; - } -} - -.placeholder-widget { - a { - text-decoration: none; - font-weight: 500; - color: $ui-highlight-color; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } -}