diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index 058a68a0991..0274bf9c6fd 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -1,6 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { + ApiAccountJSON, + ShallowApiAccountJSON, +} from 'mastodon/api_types/accounts'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; export const revealAccount = createAction<{ @@ -11,6 +14,10 @@ export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>( 'accounts/importAccounts', ); +export const importShallowAccounts = createAction<{ + accounts: ShallowApiAccountJSON[]; +}>('accounts/importShallowAccounts'); + function actionWithSkipLoadingTrue(args: Args) { return { payload: { diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 516a7a79733..a0c7c0e1930 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,6 +1,6 @@ import { importAccounts } from '../accounts_typed'; -import { normalizeStatus, normalizePoll } from './normalizer'; +import { normalizeStatus, normalizeShallowStatus, normalizePoll } from './normalizer'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; @@ -49,6 +49,10 @@ export function importFetchedAccounts(accounts) { return importAccounts({ accounts: normalAccounts }); } +export function importShallowFetchedAccounts(accounts) { + return importAccounts({ accounts }); +} + export function importFetchedStatus(status) { return importFetchedStatuses([status]); } @@ -90,6 +94,32 @@ export function importFetchedStatuses(statuses) { }; } +export function importShallowStatuses(statuses) { + return (dispatch, getState) => { + const normalStatuses = []; + const polls = []; + const filters = []; + + function processStatus(status) { + pushUnique(normalStatuses, normalizeShallowStatus(status, getState().getIn(['statuses', status.id]))); + + if (status.filtered) { + status.filtered.forEach(result => pushUnique(filters, result.filter)); + } + + if (status.poll?.id) { + pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); + } + } + + statuses.forEach(processStatus); + + dispatch(importPolls(polls)); + dispatch(importStatuses(normalStatuses)); + dispatch(importFilters(filters)); + }; +} + export function importFetchedPoll(poll) { return (dispatch, getState) => { dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))])); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index c09a3f442c7..b4f8c22f24b 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -25,13 +25,34 @@ export function normalizeFilterResult(result) { } export function normalizeStatus(status, normalOldStatus) { - const normalStatus = { ...status }; - normalStatus.account = status.account.id; + const { account, reblog, ...normalStatus } = status; - if (status.reblog && status.reblog.id) { - normalStatus.reblog = status.reblog.id; + normalStatus.account_id = account.id; + + if (reblog && reblog.id) { + normalStatus.reblog_id = reblog.id; } + if (status.card) { + normalStatus.card = { + ...status.card, + authors: status.card.authors.map(author => ({ + ...author, + account_id: author.account?.id, + account: undefined, + })), + }; + } + + return normalizeShallowStatus(normalStatus, normalOldStatus); +} + +export function normalizeShallowStatus(status, normalOldStatus) { + const { account_id, reblog_id, ...normalStatus } = status; + + normalStatus.account = account_id; + normalStatus.reblog = reblog_id; + if (status.poll && status.poll.id) { normalStatus.poll = status.poll.id; } @@ -41,8 +62,8 @@ export function normalizeStatus(status, normalOldStatus) { ...status.card, authors: status.card.authors.map(author => ({ ...author, - accountId: author.account?.id, - account: undefined, + accountId: author.account_id, + account_id: undefined, })), }; } diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 8fdec6e48bb..b407ba204ef 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -5,6 +5,7 @@ import { apiFetchNotifications, } from 'mastodon/api/notifications'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiGenericJSON } from 'mastodon/api_types/generic'; import type { ApiNotificationGroupJSON, ApiNotificationJSON, @@ -22,7 +23,12 @@ import { createDataLoadingThunk, } from 'mastodon/store/typed_functions'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; +import { importShallowAccounts } from './accounts_typed'; +import { + importFetchedAccounts, + importFetchedStatuses, + importShallowStatuses, +} from './importer'; import { NOTIFICATIONS_FILTER_SET } from './notifications'; import { saveSettings } from './settings'; @@ -32,16 +38,12 @@ function excludeAllTypesExcept(filter: string) { function dispatchAssociatedRecords( dispatch: AppDispatch, - notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], + notifications: ApiNotificationJSON[], ) { const fetchedAccounts: ApiAccountJSON[] = []; const fetchedStatuses: ApiStatusJSON[] = []; notifications.forEach((notification) => { - if ('sample_accounts' in notification) { - fetchedAccounts.push(...notification.sample_accounts); - } - if (notification.type === 'admin.report') { fetchedAccounts.push(notification.report.target_account); } @@ -62,6 +64,17 @@ function dispatchAssociatedRecords( dispatch(importFetchedStatuses(fetchedStatuses)); } +function dispatchGenericAssociatedRecords( + dispatch: AppDispatch, + apiResult: ApiGenericJSON, +) { + if (apiResult.accounts.length > 0) + dispatch(importShallowAccounts({ accounts: apiResult.accounts })); + + if (apiResult.statuses.length > 0) + dispatch(importShallowStatuses(apiResult.statuses)); +} + export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', async (_params, { getState }) => { @@ -75,15 +88,18 @@ export const fetchNotifications = createDataLoadingThunk( : excludeAllTypesExcept(activeFilter), }); }, - ({ notifications }, { dispatch }) => { - dispatchAssociatedRecords(dispatch, notifications); + ({ apiResult }, { dispatch }) => { + dispatchGenericAssociatedRecords(dispatch, apiResult); const payload: (ApiNotificationGroupJSON | NotificationGap)[] = - notifications; + apiResult.notification_groups; // TODO: might be worth not using gaps for that… // if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri }); - if (notifications.length > 1) - payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id }); + if (apiResult.notification_groups.length > 1) + payload.push({ + type: 'gap', + maxId: apiResult.notification_groups.at(-1)?.page_min_id, + }); return payload; // dispatch(submitMarkers()); @@ -95,10 +111,10 @@ export const fetchNotificationsGap = createDataLoadingThunk( async (params: { gap: NotificationGap }) => apiFetchNotifications({ max_id: params.gap.maxId }), - ({ notifications }, { dispatch }) => { - dispatchAssociatedRecords(dispatch, notifications); + ({ apiResult }, { dispatch }) => { + dispatchGenericAssociatedRecords(dispatch, apiResult); - return { notifications }; + return { apiResult }; }, ); diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts index c1ab6f70caf..df5c64788ac 100644 --- a/app/javascript/mastodon/api/notifications.ts +++ b/app/javascript/mastodon/api/notifications.ts @@ -1,17 +1,17 @@ import api, { apiRequest, getLinks } from 'mastodon/api'; -import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications'; +import type { ApiGenericJSON } from 'mastodon/api_types/generic'; export const apiFetchNotifications = async (params?: { exclude_types?: string[]; max_id?: string; }) => { - const response = await api().request({ + const response = await api().request({ method: 'GET', url: '/api/v2_alpha/notifications', params, }); - return { notifications: response.data, links: getLinks(response) }; + return { apiResult: response.data, links: getLinks(response) }; }; export const apiClearNotifications = () => diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 5bf3e64288c..ed5335edd57 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -12,8 +12,8 @@ export interface ApiAccountRoleJSON { name: string; } -// See app/serializers/rest/account_serializer.rb -export interface ApiAccountJSON { +// See app/serializers/rest/base_account_serializer.rb +export interface BaseApiAccountJSON { acct: string; avatar: string; avatar_static: string; @@ -34,14 +34,21 @@ export interface ApiAccountJSON { locked: boolean; noindex?: boolean; note: string; - roles?: ApiAccountJSON[]; + roles?: ApiAccountRoleJSON[]; statuses_count: number; uri: string; url: string; username: string; - moved?: ApiAccountJSON; suspended?: boolean; limited?: boolean; memorial?: boolean; hide_collections: boolean; } + +export interface ApiAccountJSON extends BaseApiAccountJSON { + moved?: ApiAccountJSON; +} + +export interface ShallowApiAccountJSON extends BaseApiAccountJSON { + moved_to_account_id?: string; +} diff --git a/app/javascript/mastodon/api_types/generic.ts b/app/javascript/mastodon/api_types/generic.ts new file mode 100644 index 00000000000..4b99dbe0796 --- /dev/null +++ b/app/javascript/mastodon/api_types/generic.ts @@ -0,0 +1,9 @@ +import type { ShallowApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications'; +import type { ShallowApiStatusJSON } from 'mastodon/api_types/statuses'; + +export interface ApiGenericJSON { + statuses: ShallowApiStatusJSON[]; + accounts: ShallowApiAccountJSON[]; + notification_groups: ApiNotificationGroupJSON[]; +} diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index d7cbbca73b9..933de4ee150 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -3,7 +3,7 @@ import type { AccountWarningAction } from 'mastodon/models/notification_group'; import type { ApiAccountJSON } from './accounts'; -import type { ApiReportJSON } from './reports'; +import type { ApiReportJSON, ShallowApiReportJSON } from './reports'; import type { ApiStatusJSON } from './statuses'; // See app/model/notification.rb @@ -51,7 +51,7 @@ export interface BaseNotificationGroupJSON { group_key: string; notifications_count: number; type: NotificationType; - sample_accounts: ApiAccountJSON[]; + sample_account_ids: string[]; latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly most_recent_notification_id: string; page_min_id?: string; @@ -60,7 +60,7 @@ export interface BaseNotificationGroupJSON { interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON { type: NotificationWithStatusType; - status: ApiStatusJSON; + status_id: string; } interface NotificationWithStatusJSON extends BaseNotificationJSON { @@ -70,7 +70,7 @@ interface NotificationWithStatusJSON extends BaseNotificationJSON { interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON { type: 'admin.report'; - report: ApiReportJSON; + report: ShallowApiReportJSON; } interface ReportNotificationJSON extends BaseNotificationJSON { @@ -87,20 +87,28 @@ interface SimpleNotificationJSON extends BaseNotificationJSON { type: SimpleNotificationTypes; } -export interface ApiAccountWarningJSON { +export interface BaseApiAccountWarningJSON { id: string; action: AccountWarningAction; text: string; status_ids: string[]; created_at: string; - target_account: ApiAccountJSON; appeal: unknown; } +export interface ApiAccountWarningJSON extends BaseApiAccountWarningJSON { + target_account: ApiAccountJSON; +} + +export interface ShallowApiAccountWarningJSON + extends BaseApiAccountWarningJSON { + target_account_id: string; +} + interface ModerationWarningNotificationGroupJSON extends BaseNotificationGroupJSON { type: 'moderation_warning'; - moderation_warning: ApiAccountWarningJSON; + moderation_warning: ShallowApiAccountWarningJSON; } interface ModerationWarningNotificationJSON extends BaseNotificationJSON { diff --git a/app/javascript/mastodon/api_types/reports.ts b/app/javascript/mastodon/api_types/reports.ts index b11cfdd2eb5..c24fa8df381 100644 --- a/app/javascript/mastodon/api_types/reports.ts +++ b/app/javascript/mastodon/api_types/reports.ts @@ -2,7 +2,7 @@ import type { ApiAccountJSON } from './accounts'; export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation'; -export interface ApiReportJSON { +export interface BaseApiReportJSON { id: string; action_taken: unknown; action_taken_at: unknown; @@ -12,5 +12,12 @@ export interface ApiReportJSON { created_at: string; status_ids: string[]; rule_ids: string[]; +} + +export interface ApiReportJSON extends BaseApiReportJSON { target_account: ApiAccountJSON; } + +export interface ShallowApiReportJSON extends BaseApiReportJSON { + target_account_id: string; +} diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index a934faeb7a7..dabbcad4eed 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -1,4 +1,4 @@ -// See app/serializers/rest/status_serializer.rb +// See app/serializers/rest/base_status_serializer.rb import type { ApiAccountJSON } from './accounts'; import type { ApiCustomEmojiJSON } from './custom_emoji'; @@ -58,7 +58,7 @@ export interface ApiPreviewCardJSON { authors: ApiPreviewCardAuthorJSON[]; } -export interface ApiStatusJSON { +export interface BaseApiStatusJSON { id: string; created_at: string; in_reply_to_id?: string; @@ -85,9 +85,7 @@ export interface ApiStatusJSON { content?: string; text?: string; - reblog?: ApiStatusJSON; application?: ApiStatusApplicationJSON; - account: ApiAccountJSON; media_attachments: ApiMediaAttachmentJSON[]; mentions: ApiMentionJSON[]; @@ -97,3 +95,13 @@ export interface ApiStatusJSON { card?: ApiPreviewCardJSON; poll?: ApiPollJSON; } + +export interface ApiStatusJSON extends BaseApiStatusJSON { + account: ApiAccountJSON; + reblog?: ApiStatusJSON; +} + +export interface ShallowApiStatusJSON extends BaseApiStatusJSON { + account_id: string; + reblog_id?: string; +} diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index a04ebe62915..18d1b4aa1de 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -7,6 +7,7 @@ import type { ApiAccountFieldJSON, ApiAccountRoleJSON, ApiAccountJSON, + ShallowApiAccountJSON, } from 'mastodon/api_types/accounts'; import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji'; import emojify from 'mastodon/features/emoji/emoji'; @@ -149,3 +150,32 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { note_plain: unescapeHTML(accountJSON.note), }); } + +export function createAccountFromServerShallowJSON( + serverJSON: ShallowApiAccountJSON, +) { + const { moved_to_account_id, ...accountJSON } = serverJSON; + + const emojiMap = makeEmojiMap(accountJSON.emojis); + + const displayName = + accountJSON.display_name.trim().length === 0 + ? accountJSON.username + : accountJSON.display_name; + + return AccountFactory({ + ...accountJSON, + moved: moved_to_account_id, + fields: List( + serverJSON.fields.map((field) => createAccountField(field, emojiMap)), + ), + emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))), + roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))), + display_name_html: emojify( + escapeTextContentForBrowser(displayName), + emojiMap, + ), + note_emojified: emojify(accountJSON.note, emojiMap), + note_plain: unescapeHTML(accountJSON.note), + }); +} diff --git a/app/javascript/mastodon/models/notification_group.ts b/app/javascript/mastodon/models/notification_group.ts index 5fe1e6f2e43..d6d02354d99 100644 --- a/app/javascript/mastodon/models/notification_group.ts +++ b/app/javascript/mastodon/models/notification_group.ts @@ -1,5 +1,6 @@ import type { ApiAccountRelationshipSeveranceEventJSON, + ShallowApiAccountWarningJSON, ApiAccountWarningJSON, BaseNotificationGroupJSON, ApiNotificationGroupJSON, @@ -7,14 +8,17 @@ import type { NotificationType, NotificationWithStatusType, } from 'mastodon/api_types/notifications'; -import type { ApiReportJSON } from 'mastodon/api_types/reports'; +import type { + ApiReportJSON, + ShallowApiReportJSON, +} from 'mastodon/api_types/reports'; // Maximum number of avatars displayed in a notification group // This corresponds to the max lenght of `group.sampleAccountIds` export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8; interface BaseNotificationGroup - extends Omit { + extends Omit { sampleAccountIds: string[]; } @@ -96,6 +100,14 @@ function createReportFromJSON(reportJSON: ApiReportJSON): Report { }; } +function createReportFromShallowJSON(reportJSON: ShallowApiReportJSON): Report { + const { target_account_id, ...report } = reportJSON; + return { + targetAccountId: target_account_id, + ...report, + }; +} + function createAccountWarningFromJSON( warningJSON: ApiAccountWarningJSON, ): AccountWarning { @@ -106,6 +118,16 @@ function createAccountWarningFromJSON( }; } +function createAccountWarningFromShallowJSON( + warningJSON: ShallowApiAccountWarningJSON, +): AccountWarning { + const { target_account_id, ...warning } = warningJSON; + return { + targetAccountId: target_account_id, + ...warning, + }; +} + function createAccountRelationshipSeveranceEventFromJSON( eventJson: ApiAccountRelationshipSeveranceEventJSON, ): AccountRelationshipSeveranceEvent { @@ -115,8 +137,7 @@ function createAccountRelationshipSeveranceEventFromJSON( export function createNotificationGroupFromJSON( groupJson: ApiNotificationGroupJSON, ): NotificationGroup { - const { sample_accounts, ...group } = groupJson; - const sampleAccountIds = sample_accounts.map((account) => account.id); + const { sample_account_ids: sampleAccountIds, ...group } = groupJson; switch (group.type) { case 'favourite': @@ -125,9 +146,9 @@ export function createNotificationGroupFromJSON( case 'mention': case 'poll': case 'update': { - const { status, ...groupWithoutStatus } = group; + const { status_id, ...groupWithoutStatus } = group; return { - statusId: status.id, + statusId: status_id, sampleAccountIds, ...groupWithoutStatus, }; @@ -135,7 +156,7 @@ export function createNotificationGroupFromJSON( case 'admin.report': { const { report, ...groupWithoutTargetAccount } = group; return { - report: createReportFromJSON(report), + report: createReportFromShallowJSON(report), sampleAccountIds, ...groupWithoutTargetAccount, }; @@ -151,7 +172,8 @@ export function createNotificationGroupFromJSON( const { moderation_warning, ...groupWithoutModerationWarning } = group; return { ...groupWithoutModerationWarning, - moderationWarning: createAccountWarningFromJSON(moderation_warning), + moderationWarning: + createAccountWarningFromShallowJSON(moderation_warning), sampleAccountIds, }; } diff --git a/app/javascript/mastodon/reducers/accounts.ts b/app/javascript/mastodon/reducers/accounts.ts index 5a9cc7220c7..aa39e27ca88 100644 --- a/app/javascript/mastodon/reducers/accounts.ts +++ b/app/javascript/mastodon/reducers/accounts.ts @@ -5,12 +5,19 @@ import { followAccountSuccess, unfollowAccountSuccess, importAccounts, + importShallowAccounts, revealAccount, } from 'mastodon/actions/accounts_typed'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { + ApiAccountJSON, + ShallowApiAccountJSON, +} from 'mastodon/api_types/accounts'; import { me } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; -import { createAccountFromServerJSON } from 'mastodon/models/account'; +import { + createAccountFromServerJSON, + createAccountFromServerShallowJSON, +} from 'mastodon/models/account'; const initialState = ImmutableMap(); @@ -40,6 +47,32 @@ const normalizeAccounts = ( return state; }; +const normalizeShallowAccount = ( + state: typeof initialState, + account: ShallowApiAccountJSON, +) => { + return state.set( + account.id, + createAccountFromServerShallowJSON(account).set( + 'hidden', + state.get(account.id)?.hidden === false + ? false + : account.limited || false, + ), + ); +}; + +const normalizeShallowAccounts = ( + state: typeof initialState, + accounts: ShallowApiAccountJSON[], +) => { + accounts.forEach((account) => { + state = normalizeShallowAccount(state, account); + }); + + return state; +}; + function getCurrentUser() { if (!me) throw new Error( @@ -57,6 +90,8 @@ export const accountsReducer: Reducer = ( return state.setIn([action.payload.id, 'hidden'], false); else if (importAccounts.match(action)) return normalizeAccounts(state, action.payload.accounts); + else if (importShallowAccounts.match(action)) + return normalizeShallowAccounts(state, action.payload.accounts); else if (followAccountSuccess.match(action)) { return state .update(action.payload.relationship.id, (account) => diff --git a/app/javascript/mastodon/reducers/notification_groups.ts b/app/javascript/mastodon/reducers/notification_groups.ts index e59f3e7ca11..8d4fba269c0 100644 --- a/app/javascript/mastodon/reducers/notification_groups.ts +++ b/app/javascript/mastodon/reducers/notification_groups.ts @@ -296,7 +296,9 @@ export const notificationGroupsReducer = createReducer( updateLastReadId(state); }) .addCase(fetchNotificationsGap.fulfilled, (state, action) => { - const { notifications } = action.payload; + const { + apiResult: { notification_groups: notifications }, + } = action.payload; // find the gap in the existing notifications const gapIndex = state.groups.findIndex(