Merge pull request #141 from mkljczk/pl-api-updates

This commit is contained in:
marcin mikołajczak 2024-11-03 20:31:31 +01:00 committed by GitHub
commit d0a036c63c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 642 additions and 235 deletions

View file

@ -1,3 +1,5 @@
import omit from 'lodash.omit';
import pick from 'lodash.pick';
import * as v from 'valibot';
import {
@ -27,6 +29,7 @@ import {
contextSchema,
conversationSchema,
credentialAccountSchema,
credentialApplicationSchema,
customEmojiSchema,
domainBlockSchema,
emojiReactionSchema,
@ -70,6 +73,7 @@ import {
trendsLinkSchema,
webPushSubscriptionSchema,
} from './entities';
import { GroupedNotificationsResults, groupedNotificationsResultsSchema, NotificationGroup } from './entities/grouped-notifications-results';
import { filteredArray } from './entities/utils';
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA } from './features';
import {
@ -107,6 +111,7 @@ import {
MuteAccountParams,
UpdateFilterParams,
} from './params/filtering';
import { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams } from './params/grouped-notifications';
import {
CreateGroupParams,
GetGroupBlocksParams,
@ -189,12 +194,14 @@ import request, { getNextLink, getPrevLink, type RequestBody, RequestMeta } from
import { buildFullPath } from './utils/url';
import type {
Account,
AdminAccount,
AdminAnnouncement,
AdminModerationLogEntry,
AdminReport,
GroupRole,
Instance,
Notification,
PleromaConfig,
Status,
StreamingEvent,
@ -228,7 +235,9 @@ import type {
AdminUpdateRuleParams,
AdminUpdateStatusParams,
} from './params/admin';
import type { PaginatedResponse } from './responses';
import type { PaginatedResponse, PaginatedSingleResponse } from './responses';
const GROUPED_TYPES = ['favourite', 'reblog', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'];
/**
* @category Clients
@ -292,6 +301,28 @@ class PlApiClient {
};
};
#paginatedSingleGet = async <T>(input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>): Promise<PaginatedSingleResponse<T>> => {
const getMore = (input: string | null) => input ? async () => {
const response = await this.request(input);
return {
previous: getMore(getPrevLink(response)),
next: getMore(getNextLink(response)),
items: v.parse(schema, response.json),
partial: response.status === 206,
};
} : null;
const response = await this.request(input, body);
return {
previous: getMore(getPrevLink(response)),
next: getMore(getNextLink(response)),
items: v.parse(schema, response.json),
partial: response.status === 206,
};
};
#paginatedPleromaAccounts = async (params: {
query?: string;
filters?: string;
@ -353,6 +384,65 @@ class PlApiClient {
};
};
#groupNotifications = ({ previous, next, items, ...response }: PaginatedResponse<Notification>, params?: GetGroupedNotificationsParams): PaginatedSingleResponse<GroupedNotificationsResults> => {
const notificationGroups: Array<NotificationGroup> = [];
for (const notification of items) {
let existingGroup: NotificationGroup | undefined;
if ((params?.grouped_types || GROUPED_TYPES).includes(notification.type)) {
existingGroup = notificationGroups
.find(notificationGroup =>
notificationGroup.type === notification.type
&& ((notification.type === 'emoji_reaction' && notificationGroup.type === 'emoji_reaction') ? notification.emoji === notificationGroup.emoji : true)
// @ts-ignore
&& notificationGroup.status_id === notification.status?.id,
);
}
if (existingGroup) {
existingGroup.notifications_count += 1;
existingGroup.page_min_id = notification.id;
existingGroup.sample_account_ids.push(notification.account.id);
} else {
notificationGroups.push({
...(omit(notification, ['account', 'status', 'target'])),
group_key: notification.id,
notifications_count: 1,
most_recent_notification_id: notification.id,
page_min_id: notification.id,
page_max_id: notification.id,
latest_page_notification_at: notification.created_at,
sample_account_ids: [notification.account.id],
// @ts-ignore
status_id: notification.status?.id,
// @ts-ignore
target_id: notification.target?.id,
});
}
}
const groupedNotificationsResults: GroupedNotificationsResults = {
accounts: Object.values(items.reduce<Record<string, Account>>((accounts, notification) => {
accounts[notification.account.id] = notification.account;
if ('target' in notification) accounts[notification.target.id] = notification.target;
return accounts;
}, {})),
statuses: Object.values(items.reduce<Record<string, Status>>((statuses, notification) => {
if ('status' in notification) statuses[notification.status.id] = notification.status;
return statuses;
}, {})),
notification_groups: notificationGroups,
};
return {
...response,
previous: previous ? async () => this.#groupNotifications(await previous(), params) : null,
next: next ? async () => this.#groupNotifications(await next(), params) : null,
items: groupedNotificationsResults,
};
};
/** Register client applications that can be used to obtain OAuth tokens. */
public readonly apps = {
/**
@ -363,7 +453,7 @@ class PlApiClient {
createApplication: async (params: CreateApplicationParams) => {
const response = await this.request('/api/v1/apps', { method: 'POST', body: params });
return v.parse(applicationSchema, response.json);
return v.parse(credentialApplicationSchema, response.json);
},
/**
@ -2705,6 +2795,108 @@ class PlApiClient {
};
/**
* It is recommended to only use this with features{@link Features['groupedNotifications']} available. However, there is a fallback that groups the notifications client-side.
*/
public readonly groupedNotifications = {
/**
* Get all grouped notifications
* Return grouped notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.
*
* Requires features{@link Features['groupedNotifications']}.
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped}
*/
getGroupedNotifications: async (params: GetGroupedNotificationsParams, meta?: RequestMeta) => {
if (this.features.groupedNotifications) {
return this.#paginatedSingleGet('/api/v2/notifications', { ...meta, params }, groupedNotificationsResultsSchema);
} else {
const response = await this.notifications.getNotifications(
pick(params, ['max_id', 'since_id', 'limit', 'min_id', 'types', 'exclude_types', 'account_id', 'include_filtered']),
);
return this.#groupNotifications(response, params);
}
},
/**
* Get a single notification group
* View information about a specific notification group with a given group key.
*
* Requires features{@link Features['groupedNotifications']}.
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group}
*/
getNotificationGroup: async (groupKey: string) => {
if (this.features.groupedNotifications) {
const response = await this.request(`/api/v2/notifications/${groupKey}`);
return v.parse(groupedNotificationsResultsSchema, response.json);
} else {
const response = await this.request(`/api/v1/notifications/${groupKey}`);
return this.#groupNotifications({
previous: null,
next: null,
items: [response.json],
partial: false,
}).items;
}
},
/**
* Dismiss a single notification group
* Dismiss a single notification group from the server.
*
* Requires features{@link Features['groupedNotifications']}.
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group}
*/
dismissNotificationGroup: async (groupKey: string) => {
if (this.features.groupedNotifications) {
const response = await this.request(`/api/v2/notifications/${groupKey}/dismiss`, { method: 'POST' });
return response.json as {};
} else {
return this.notifications.dismissNotification(groupKey);
}
},
/**
* Get accounts of all notifications in a notification group
*
* Requires features{@link Features['groupedNotifications']}.
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts}
*/
getNotificationGroupAccounts: async (groupKey: string) => {
if (this.features.groupedNotifications) {
const response = await this.request(`/api/v2/notifications/${groupKey}/accounts`);
return v.parse(filteredArray(accountSchema), response.json);
} else {
return (await (this.groupedNotifications.getNotificationGroup(groupKey))).accounts;
}
},
/**
* Get the number of unread notifications
* Get the (capped) number of unread notification groups for the current user. A notification is considered unread if it is more recent than the notifications read marker. Because the count is dependant on the parameters, it is computed every time and is thus a relatively slow operation (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped.
*
* Requires features{@link Features['groupedNotifications']}.
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count}
*/
getUnreadNotificationGroupCount: async (params: GetUnreadNotificationGroupCountParams) => {
if (this.features.groupedNotifications) {
const response = await this.request('/api/v2/notifications/unread_count', { params });
return v.parse(v.object({
count: v.number(),
}), response.json);
} else {
return this.notifications.getUnreadNotificationCount(
pick(params || {}, ['max_id', 'since_id', 'limit', 'min_id', 'types', 'exclude_types', 'account_id']),
);
}
},
};
public readonly pushNotifications = {
/**
* Subscribe to push notifications

View file

@ -12,9 +12,6 @@ const applicationSchema = v.pipe(v.any(), v.transform((application) => ({
})), v.object({
name: v.fallback(v.string(), ''),
website: v.fallback(v.optional(v.string()), undefined),
client_id: v.fallback(v.optional(v.string()), undefined),
client_secret: v.fallback(v.optional(v.string()), undefined),
client_secret_expires_at: v.fallback(v.optional(v.string()), undefined),
redirect_uris: filteredArray(v.string()),
id: v.fallback(v.optional(v.string()), undefined),
@ -27,4 +24,21 @@ const applicationSchema = v.pipe(v.any(), v.transform((application) => ({
type Application = v.InferOutput<typeof applicationSchema>;
export { applicationSchema, type Application };
/**
* @category Schemas
* @see {@link https://docs.joinmastodon.org/entities/Application/#CredentialApplication}
*/
const credentialApplicationSchema = v.pipe(
applicationSchema.pipe[0],
applicationSchema.pipe[1],
v.object({
...applicationSchema.pipe[2].entries,
client_id: v.string(),
client_secret: v.string(),
client_secret_expires_at: v.fallback(v.optional(v.string()), undefined),
}),
);
type CredentialApplication = v.InferOutput<typeof credentialApplicationSchema>;
export { applicationSchema, credentialApplicationSchema, type Application, type CredentialApplication };

View file

@ -0,0 +1,151 @@
import pick from 'lodash.pick';
import * as v from 'valibot';
import { accountSchema } from './account';
import { accountWarningSchema } from './account-warning';
import { chatMessageSchema } from './chat-message';
import { relationshipSeveranceEventSchema } from './relationship-severance-event';
import { reportSchema } from './report';
import { statusSchema } from './status';
import { datetimeSchema, filteredArray } from './utils';
const partialAccountWithAvatarSchema = v.object({
id: v.string(),
acct: v.string(),
url: v.pipe(v.string(), v.url()),
avatar: v.pipe(v.string(), v.url()),
avatar_static: v.pipe(v.string(), v.url()),
locked: v.boolean(),
bot: v.boolean(),
});
const baseNotificationGroupSchema = v.object({
group_key: v.string(),
notifications_count: v.pipe(v.number(), v.integer()),
most_recent_notification_id: v.string(),
page_min_id: v.fallback(v.optional(v.string()), undefined),
page_max_id: v.fallback(v.optional(v.string()), undefined),
latest_page_notification_at: v.fallback(v.optional(datetimeSchema), undefined),
sample_account_ids: v.array(v.string()),
is_muted: v.fallback(v.optional(v.boolean()), undefined),
is_seen: v.fallback(v.optional(v.boolean()), undefined),
});
const accountNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.picklist(['follow', 'follow_request', 'admin.sign_up', 'bite']),
});
const mentionNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('mention'),
subtype: v.fallback(v.nullable(v.picklist(['reply'])), null),
status_id: v.string(),
});
const statusNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.picklist(['status', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']),
status_id: v.string(),
});
const reportNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('admin.report'),
report: reportSchema,
});
const severedRelationshipNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('severed_relationships'),
relationship_severance_event: relationshipSeveranceEventSchema,
});
const moderationWarningNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('moderation_warning'),
moderation_warning: accountWarningSchema,
});
const moveNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('move'),
target_id: v.string(),
});
const emojiReactionNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('emoji_reaction'),
emoji: v.string(),
emoji_url: v.fallback(v.nullable(v.string()), null),
status_id: v.string(),
});
const chatMentionNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.literal('chat_mention'),
chat_message: chatMessageSchema,
});
const eventParticipationRequestNotificationGroupSchema = v.object({
...baseNotificationGroupSchema.entries,
type: v.picklist(['participation_accepted', 'participation_request']),
status_id: v.string(),
participation_message: v.fallback(v.nullable(v.string()), null),
});
/**
* @category Schemas
* @see {@link https://docs.joinmastodon.org/entities/Notification/}
* */
const notificationGroupSchema: v.BaseSchema<any, NotificationGroup, v.BaseIssue<unknown>> = v.pipe(
v.any(),
v.transform((notification: any) => ({
group_key: `ungrouped-${notification.id}`,
...pick(notification.pleroma || {}, ['is_muted', 'is_seen']),
...notification,
type: notification.type === 'pleroma:report'
? 'admin.report'
: notification.type?.replace(/^pleroma:/, ''),
})),
v.variant('type', [
accountNotificationGroupSchema,
mentionNotificationGroupSchema,
statusNotificationGroupSchema,
reportNotificationGroupSchema,
severedRelationshipNotificationGroupSchema,
moderationWarningNotificationGroupSchema,
moveNotificationGroupSchema,
emojiReactionNotificationGroupSchema,
chatMentionNotificationGroupSchema,
eventParticipationRequestNotificationGroupSchema,
])) as any;
type NotificationGroup = v.InferOutput<
| typeof accountNotificationGroupSchema
| typeof mentionNotificationGroupSchema
| typeof statusNotificationGroupSchema
| typeof reportNotificationGroupSchema
| typeof severedRelationshipNotificationGroupSchema
| typeof moderationWarningNotificationGroupSchema
| typeof moveNotificationGroupSchema
| typeof emojiReactionNotificationGroupSchema
| typeof chatMentionNotificationGroupSchema
| typeof eventParticipationRequestNotificationGroupSchema
>;
/**
* @category Schemas
* @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#GroupedNotificationsResults}
*/
const groupedNotificationsResultsSchema = v.object({
accounts: filteredArray(accountSchema),
partial_accounts: v.fallback(v.optional(v.array(partialAccountWithAvatarSchema)), undefined),
statuses: filteredArray(statusSchema),
notification_groups: filteredArray(notificationGroupSchema),
});
type GroupedNotificationsResults = v.InferOutput<typeof groupedNotificationsResultsSchema>;
export { notificationGroupSchema, groupedNotificationsResultsSchema, type NotificationGroup, type GroupedNotificationsResults };

View file

@ -41,6 +41,7 @@ export * from './filter';
export * from './group';
export * from './group-member';
export * from './group-relationship';
export * from './grouped-notifications-results';
export * from './instance';
export * from './interaction-policy';
export * from './interaction-request';

View file

@ -619,6 +619,15 @@ const getFeatures = (instance: Instance) => {
v.software === PLEROMA,
]),
/**
* @see GET /api/v2/notifications/:group_key
* @see GET /api/v2/notifications/:group_key
* @see POST /api/v2/notifications/:group_key/dismiss
* @see GET /api/v2/notifications/:group_key/accounts
* @see GET /api/v2/notifications/unread_count
*/
groupedNotifications: instance.api_versions.mastodon >= 2,
/**
* Groups.
* @see POST /api/v1/groups

View file

@ -0,0 +1,31 @@
import type { PaginationParams } from './common';
interface GetGroupedNotificationsParams extends PaginationParams {
/** Types to include in the result. */
types?: Array<string>;
/** Types to exclude from the results. */
exclude_types?: Array<string>;
/** Return only notifications received from the specified account. */
acccount_id?: string;
/** One of `full` (default) or `partial_avatars`. When set to `partial_avatars`, some accounts will not be rendered in full in the returned `accounts` list but will be instead returned in stripped-down form in the `partial_accounts` list. The most recent account in a notification group is always rendered in full in the `accounts` attribute. */
expand_accounts?: 'full' | 'partial_avatars';
/** Restrict which notification types can be grouped. Use this if there are notification types for which your client does not support grouping. If omitted, the server will group notifications of all types it supports (currently, `favourite`, `follow` and `reblog`). If you do not want any notification grouping, use GET `/api/v1/notifications` instead. Notifications that would be grouped if not for this parameter will instead be returned as individual single-notification groups with a unique `group_key` that can be assumed to be of the form `ungrouped-{notification_id}`. Please note that neither the streaming API nor the individual notification APIs are aware of this parameter and will always include a “proper” `group_key` that can be different from what is returned here, meaning that you may have to ignore `group_key` for such notifications that you do not want grouped and use `ungrouped-{notification_id}` instead for consistency. */
grouped_types?: Array<string>;
/** Whether to include notifications filtered by the users NotificationPolicy. Defaults to false. */
include_filtered?: boolean;
}
interface GetUnreadNotificationGroupCountParams {
/** Maximum number of results to return. Defaults to 100 notifications. Max 1000 notifications. */
limit?: number;
/** Types of notifications that should count towards unread notifications. */
types?: Array<string>;
/** Types of notifications that should not count towards unread notifications. */
exclude_types?: Array<string>;
/** Only count unread notifications received from the specified account. */
account_id?: string;
/** Restrict which notification types can be grouped. Use this if there are notification types for which your client does not support grouping. If omitted, the server will group notifications of all types it supports (currently, `favourite`, `follow` and `reblog`). If you do not want any notification grouping, use GET /api/v1/notifications/unread_count instead. */
grouped_types?: Array<string>;
}
export type { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams };

View file

@ -4,6 +4,7 @@ export * from './apps';
export * from './chats';
export * from './events';
export * from './filtering';
export * from './grouped-notifications';
export * from './groups';
export * from './instance';
export * from './interaction-requests';

View file

@ -1,11 +1,14 @@
interface PaginatedResponse<T> {
previous: (() => Promise<PaginatedResponse<T>>) | null;
next: (() => Promise<PaginatedResponse<T>>) | null;
items: Array<T>;
interface PaginatedSingleResponse<T> {
previous: (() => Promise<PaginatedSingleResponse<T>>) | null;
next: (() => Promise<PaginatedSingleResponse<T>>) | null;
items: T;
partial: boolean;
total?: number;
}
type PaginatedResponse<T> = PaginatedSingleResponse<Array<T>>;
export type {
PaginatedSingleResponse,
PaginatedResponse,
};

View file

@ -1,6 +1,6 @@
{
"name": "pl-api",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
"repository": {
@ -20,6 +20,7 @@
"devDependencies": {
"@stylistic/eslint-plugin": "^2.8.0",
"@types/http-link-header": "^1.0.7",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pick": "^4.4.9",
"@types/node": "^22.7.4",
"@types/semver": "^7.5.8",
@ -41,6 +42,7 @@
"dependencies": {
"blurhash": "^2.0.5",
"http-link-header": "^1.1.3",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"object-to-formdata": "^4.5.1",
"query-string": "^9.1.0",

View file

@ -478,6 +478,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash.omit@^4.5.9":
version "4.5.9"
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.9.tgz#cf4744d034961406d6dc41d9cd109773a9ed8fe3"
integrity sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw==
dependencies:
"@types/lodash" "*"
"@types/lodash.pick@^4.4.9":
version "4.4.9"
resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.9.tgz#06f7d88faa81a6c5665584778aea7b1374a1dc5b"
@ -2002,6 +2009,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.omit@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"

View file

@ -102,7 +102,7 @@
"mini-css-extract-plugin": "^2.9.1",
"multiselect-react-dropdown": "^2.0.25",
"path-browserify": "^1.0.1",
"pl-api": "^0.1.5",
"pl-api": "^0.1.7",
"postcss": "^8.4.47",
"process": "^0.11.10",
"punycode": "^2.1.1",

View file

@ -47,11 +47,11 @@ const externalAuthorize = (instance: Instance, baseURL: string) =>
const scopes = getInstanceScopes(instance);
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
const { client_id, redirect_uri } = app as Record<string, string>;
const { client_id, redirect_uri } = app;
const query = new URLSearchParams({
client_id,
redirect_uri,
redirect_uri: redirect_uri || app.redirect_uris[0]!,
response_type: 'code',
scope: scopes,
});

View file

@ -4,7 +4,7 @@ import { defineMessages } from 'react-intl';
import { getClient } from 'pl-fe/api';
import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification';
import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers/notification';
import { normalizeNotification } from 'pl-fe/normalizers/notification';
import { getFilters, regexFromFilters } from 'pl-fe/selectors';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { isLoggedIn } from 'pl-fe/utils/auth';
@ -18,7 +18,7 @@ import { importEntities } from './importer';
import { saveMarker } from './markers';
import { saveSettings } from './settings';
import type { Account, Notification as BaseNotification, PaginatedResponse, Status } from 'pl-api';
import type { Notification as BaseNotification, GetGroupedNotificationsParams, GroupedNotificationsResults, NotificationGroup, PaginatedSingleResponse } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const;
@ -58,8 +58,8 @@ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array<BaseNotification>) => {
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array<NotificationGroup>) => {
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.sample_account_ids).flat();
if (accountIds.length > 0) {
dispatch(fetchRelationships(accountIds));
@ -76,13 +76,16 @@ const updateNotifications = (notification: BaseNotification) =>
statuses: [getNotificationStatus(notification)],
}));
if (showInColumn) {
const normalizedNotification = normalizeNotification(notification);
dispatch({
type: NOTIFICATIONS_UPDATE,
notification: normalizeNotification(notification),
notification: normalizedNotification,
});
fetchRelatedRelationships(dispatch, [notification]);
fetchRelatedRelationships(dispatch, [normalizedNotification]);
}
};
@ -195,7 +198,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
}
}
const params: Record<string, any> = {
const params: GetGroupedNotificationsParams = {
max_id: maxId,
};
@ -203,7 +206,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
if (features.notificationsIncludeTypes) {
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
} else {
params.exclude_types = EXCLUDE_TYPES;
params.exclude_types = [...EXCLUDE_TYPES];
}
} else {
const filtered = FILTER_TYPES[activeFilter] || [activeFilter];
@ -215,51 +218,65 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
}
if (!maxId && notifications.items.size > 0) {
params.since_id = notifications.getIn(['items', 0, 'id']);
params.since_id = notifications.items.first()?.page_max_id;
}
dispatch(expandNotificationsRequest());
return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => {
const entries = (response.items).reduce((acc, item) => {
if (item.account?.id) {
acc.accounts[item.account.id] = item.account;
}
// Used by Move notification
if (item.type === 'move' && item.target.id) {
acc.accounts[item.target.id] = item.target;
}
// TODO actually check for type
// @ts-ignore
if (item.status?.id) {
// @ts-ignore
acc.statuses[item.status.id] = item.status;
}
return acc;
}, { accounts: {}, statuses: {} } as { accounts: Record<string, Account>; statuses: Record<string, Status> });
return getClient(state).groupedNotifications.getGroupedNotifications(params, { signal: abortExpandNotifications.signal }).then(({ items: { accounts, statuses, notification_groups }, next }) => {
dispatch(importEntities({
accounts: Object.values(entries.accounts),
statuses: Object.values(entries.statuses),
accounts,
statuses,
}));
const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items);
dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next));
fetchRelatedRelationships(dispatch, response.items);
dispatch(expandNotificationsSuccess(notification_groups, next));
fetchRelatedRelationships(dispatch, notification_groups);
done();
}).catch(error => {
dispatch(expandNotificationsFail(error));
done();
});
// return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => {
// const entries = (response.items).reduce((acc, item) => {
// if (item.account?.id) {
// acc.accounts[item.account.id] = item.account;
// }
// // Used by Move notification
// if (item.type === 'move' && item.target.id) {
// acc.accounts[item.target.id] = item.target;
// }
// // TODO actually check for type
// // @ts-ignore
// if (item.status?.id) {
// // @ts-ignore
// acc.statuses[item.status.id] = item.status;
// }
// return acc;
// }, { accounts: {}, statuses: {} } as { accounts: Record<string, Account>; statuses: Record<string, Status> });
// dispatch(importEntities({
// accounts: Object.values(entries.accounts),
// statuses: Object.values(entries.statuses),
// }));
// const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items);
// dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next));
// fetchRelatedRelationships(dispatch, response.items);
// done();
// }).catch(error => {
// dispatch(expandNotificationsFail(error));
// done();
// });
};
const expandNotificationsRequest = () => ({ type: NOTIFICATIONS_EXPAND_REQUEST });
const expandNotificationsSuccess = (notifications: Array<Notification>, next: (() => Promise<PaginatedResponse<BaseNotification>>) | null) => ({
const expandNotificationsSuccess = (notifications: Array<NotificationGroup>, next: (() => Promise<PaginatedSingleResponse<GroupedNotificationsResults>>) | null) => ({
type: NOTIFICATIONS_EXPAND_SUCCESS,
notifications,
next,
@ -297,7 +314,7 @@ const markReadNotifications = () =>
if (!isLoggedIn(getState)) return;
const state = getState();
const topNotificationId = state.notifications.items.first()?.id;
const topNotificationId = state.notifications.items.first()?.page_max_id;
const lastReadId = state.notifications.lastRead;
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {

View file

@ -13,8 +13,6 @@ import type { AppDispatch, RootState } from 'pl-fe/store';
const FE_NAME = 'pl_fe';
const getAccount = makeGetAccount();
/** Options when changing/saving settings. */
type SettingOpts = {
/** Whether to display an alert when settings are saved. */
@ -71,7 +69,7 @@ const updateSettingsStore = (settings: any) =>
},
}));
} else {
const accountUrl = getAccount(state, state.me as string)!.url;
const accountUrl = makeGetAccount()(state, state.me as string)!.url;
return updateAuthAccount(accountUrl, settings);
}

View file

@ -16,7 +16,7 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { getBaseURL } from 'pl-fe/utils/accounts';
import type { Token } from 'pl-api';
import type { CredentialApplication, Token } from 'pl-api';
const messages = defineMessages({
heading: { id: 'column.app_create', defaultMessage: 'Create app' },
@ -53,7 +53,7 @@ const CreateApp: React.FC = () => {
});
};
const handleCreateToken = (app: Record<string, string>) => {
const handleCreateToken = (app: CredentialApplication) => {
const baseURL = getBaseURL(account!);
const tokenParams = {

View file

@ -24,11 +24,9 @@ import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { NotificationType } from 'pl-fe/utils/notification';
import type { Notification as BaseNotification } from 'pl-api';
import type { NotificationGroup } from 'pl-api';
import type { Account } from 'pl-fe/normalizers/account';
import type { Notification as NotificationEntity } from 'pl-fe/normalizers/notification';
import type { Status as StatusEntity } from 'pl-fe/normalizers/status';
import type { MinifiedNotification } from 'pl-fe/reducers/notifications';
const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: string) => {
const output = [message];
@ -184,13 +182,13 @@ const avatarSize = 48;
interface INotification {
hidden?: boolean;
notification: MinifiedNotification;
notification: NotificationGroup;
onMoveUp?: (notificationId: string) => void;
onMoveDown?: (notificationId: string) => void;
onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void;
}
const getNotificationStatus = (n: NotificationEntity | BaseNotification) => {
const getNotificationStatus = (n: Pick<NotificationGroup, 'type'> & ({ status: StatusEntity } | { })) => {
if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type))
// @ts-ignore
return n.status;
@ -207,15 +205,17 @@ const Notification: React.FC<INotification> = (props) => {
const { me } = useLoggedIn();
const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const notification = useAppSelector((state) => getNotification(state, props.notification));
const status = getNotificationStatus(notification);
const history = useHistory();
const intl = useIntl();
const instance = useInstance();
const type = notification.type;
const { account, accounts } = notification;
const status = getNotificationStatus(notification);
const { accounts } = notification;
const account = accounts[0];
const getHandlers = () => ({
reply: handleMention,
@ -289,13 +289,13 @@ const Notification: React.FC<INotification> = (props) => {
const handleMoveUp = () => {
if (onMoveUp) {
onMoveUp(notification.id);
onMoveUp(notification.group_key);
}
};
const handleMoveDown = () => {
if (onMoveDown) {
onMoveDown(notification.id);
onMoveDown(notification.group_key);
}
};
@ -393,7 +393,7 @@ const Notification: React.FC<INotification> = (props) => {
name: account && typeof account === 'object' ? account.acct : '',
targetName,
}),
notification.created_at,
notification.latest_page_notification_at!,
)
);
@ -433,7 +433,7 @@ const Notification: React.FC<INotification> = (props) => {
truncate
data-testid='message'
>
<RelativeTimestamp timestamp={notification.created_at} theme='muted' size='sm' className='whitespace-nowrap' />
<RelativeTimestamp timestamp={notification.latest_page_notification_at!} theme='muted' size='sm' className='whitespace-nowrap' />
</Text>
</div>
)}

View file

@ -32,7 +32,7 @@ const messages = defineMessages({
const getNotifications = createSelector([
(state: RootState) => state.notifications.items.toList(),
], (notifications) => notifications.filter(item => item !== null && !item.duplicate));
], (notifications) => notifications.filter(item => item !== null));
const Notifications = () => {
const dispatch = useAppDispatch();
@ -54,8 +54,13 @@ const Notifications = () => {
// };
const handleLoadOlder = useCallback(debounce(() => {
const last = notifications.last();
dispatch(expandNotifications({ maxId: last && last.id }));
const minId = notifications.reduce<string | undefined>(
(minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId
? minId
: notification.page_min_id,
undefined,
);
dispatch(expandNotifications({ maxId: minId }));
}, 300, { leading: true }), [notifications]);
const handleScroll = useCallback(debounce((startIndex?: number) => {
@ -63,12 +68,12 @@ const Notifications = () => {
}, 100), []);
const handleMoveUp = (id: string) => {
const elementIndex = notifications.findIndex(item => item !== null && item.id === id) - 1;
const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) - 1;
_selectChild(elementIndex);
};
const handleMoveDown = (id: string) => {
const elementIndex = notifications.findIndex(item => item !== null && item.id === id) + 1;
const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) + 1;
_selectChild(elementIndex);
};
@ -111,7 +116,7 @@ const Notifications = () => {
} else if (notifications.size > 0 || hasMore) {
scrollableContent = notifications.map((item) => (
<Notification
key={item.id}
key={item.group_key}
notification={item}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}

View file

@ -1,61 +1,20 @@
import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification';
import omit from 'lodash/omit';
import { normalizeAccount } from './account';
import type { Notification as BaseNotification, NotificationGroup } from 'pl-api';
import type { OrderedMap as ImmutableOrderedMap } from 'immutable';
import type { Notification as BaseNotification } from 'pl-api';
import type { MinifiedNotification } from 'pl-fe/reducers/notifications';
const STATUS_NOTIFICATION_TYPES = [
'favourite',
'reblog',
'emoji_reaction',
'event_reminder',
'participation_accepted',
'participation_request',
];
const normalizeNotification = (notification: BaseNotification) => ({
...notification,
duplicate: false,
account: normalizeAccount(notification.account),
account_id: notification.account.id,
accounts: [normalizeAccount(notification.account)],
account_ids: [notification.account.id],
const normalizeNotification = (notification: BaseNotification): NotificationGroup => ({
...(omit(notification, ['account', 'status', 'target'])),
group_key: notification.id,
notifications_count: 1,
most_recent_notification_id: notification.id,
page_min_id: notification.id,
page_max_id: notification.id,
latest_page_notification_at: notification.created_at,
sample_account_ids: [notification.account.id],
// @ts-ignore
status_id: notification.status?.id,
// @ts-ignore
target_id: notification.target?.id,
});
const normalizeNotifications = (notifications: Array<BaseNotification>, stateNotifications?: ImmutableOrderedMap<string, MinifiedNotification>) => {
const deduplicatedNotifications: Notification[] = [];
for (const notification of notifications) {
const existingNotification = stateNotifications?.get(notification.id);
// Do not update grouped notifications
if (existingNotification && (existingNotification.duplicate || existingNotification.account_ids.length)) continue;
if (STATUS_NOTIFICATION_TYPES.includes(notification.type)) {
const existingNotification = deduplicatedNotifications
.find(deduplicated =>
deduplicated.type === notification.type
&& ((notification.type === 'emoji_reaction' && deduplicated.type === 'emoji_reaction') ? notification.emoji === deduplicated.emoji : true)
&& getNotificationStatus(deduplicated)?.id === getNotificationStatus(notification)?.id,
);
if (existingNotification) {
existingNotification.accounts.push(normalizeAccount(notification.account));
existingNotification.account_ids.push(notification.account.id);
deduplicatedNotifications.push({ ...normalizeNotification(notification), duplicate: true });
} else {
deduplicatedNotifications.push(normalizeNotification(notification));
}
} else {
deduplicatedNotifications.push(normalizeNotification(notification));
}
}
return deduplicatedNotifications;
};
type Notification = ReturnType<typeof normalizeNotification>;
export { normalizeNotification, normalizeNotifications, type Notification };
export { normalizeNotification };

View file

@ -1,6 +1,6 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import trim from 'lodash/trim';
import { applicationSchema, PlApiClient, tokenSchema, type Application, type CredentialAccount, type Token } from 'pl-api';
import { applicationSchema, PlApiClient, tokenSchema, type CredentialAccount, type CredentialApplication, type Token } from 'pl-api';
import * as v from 'valibot';
import { MASTODON_PRELOAD_IMPORT, type PreloadAction } from 'pl-fe/actions/preload';
@ -33,7 +33,7 @@ const AuthUserRecord = ImmutableRecord({
});
const ReducerRecord = ImmutableRecord({
app: null as Application | null,
app: null as CredentialApplication | null,
tokens: ImmutableMap<string, Token>(),
users: ImmutableMap<string, AuthUser>(),
me: null as string | null,
@ -145,7 +145,10 @@ const sanitizeState = (state: State) => {
});
};
const persistAuth = (state: State) => localStorage.setItem(STORAGE_KEY, JSON.stringify(state.delete('client').toJS()));
const persistAuth = (state: State) => {
const { client, ...data } = state.toJS();
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
};
const persistSession = (state: State) => {
const me = state.me;

View file

@ -1,5 +1,4 @@
import { Record as ImmutableRecord, OrderedMap as ImmutableOrderedMap } from 'immutable';
import omit from 'lodash/omit';
import {
ACCOUNT_BLOCK_SUCCESS,
@ -27,8 +26,7 @@ import {
} from '../actions/notifications';
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
import type { AccountWarning, Notification as BaseNotification, Markers, PaginatedResponse, Relationship, RelationshipSeveranceEvent, Report } from 'pl-api';
import type { Notification } from 'pl-fe/normalizers/notification';
import type { Notification as BaseNotification, Markers, NotificationGroup, PaginatedResponse, Relationship } from 'pl-api';
import type { AnyAction } from 'redux';
const QueuedNotificationRecord = ImmutableRecord({
@ -38,7 +36,7 @@ const QueuedNotificationRecord = ImmutableRecord({
});
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedMap<string, MinifiedNotification>(),
items: ImmutableOrderedMap<string, NotificationGroup>(),
hasMore: true,
top: false,
unread: 0,
@ -54,103 +52,103 @@ type QueuedNotification = ReturnType<typeof QueuedNotificationRecord>;
const parseId = (id: string | number) => parseInt(id as string, 10);
// For sorting the notifications
const comparator = (a: Pick<Notification, 'id'>, b: Pick<Notification, 'id'>) => {
const parse = (m: Pick<Notification, 'id'>) => parseId(m.id);
const comparator = (a: Pick<NotificationGroup, 'group_key'>, b: Pick<NotificationGroup, 'group_key'>) => {
const parse = (m: Pick<NotificationGroup, 'group_key'>) => parseId(m.group_key);
if (parse(a) < parse(b)) return 1;
if (parse(a) > parse(b)) return -1;
return 0;
};
const minifyNotification = (notification: Notification) => {
// @ts-ignore
const minifiedNotification: {
duplicate: boolean;
account_id: string;
account_ids: string[];
created_at: string;
id: string;
} & (
| { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
| {
type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
status_id: string;
}
| {
type: 'admin.report';
report: Report;
}
| {
type: 'severed_relationships';
relationship_severance_event: RelationshipSeveranceEvent;
}
| {
type: 'moderation_warning';
moderation_warning: AccountWarning;
}
| {
type: 'move';
target_id: string;
}
| {
type: 'emoji_reaction';
emoji: string;
emoji_url: string | null;
status_id: string;
}
| {
type: 'chat_mention';
chat_message_id: string;
}
| {
type: 'participation_accepted' | 'participation_request';
status_id: string;
participation_message: string | null;
}
) = {
...omit(notification, ['account', 'accounts']),
created_at: notification.created_at,
id: notification.id,
type: notification.type,
};
// const minifyNotification = (notification: Notification) => {
// // @ts-ignore
// const minifiedNotification: {
// duplicate: boolean;
// account_id: string;
// account_ids: string[];
// created_at: string;
// id: string;
// } & (
// | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
// | {
// type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
// status_id: string;
// }
// | {
// type: 'admin.report';
// report: Report;
// }
// | {
// type: 'severed_relationships';
// relationship_severance_event: RelationshipSeveranceEvent;
// }
// | {
// type: 'moderation_warning';
// moderation_warning: AccountWarning;
// }
// | {
// type: 'move';
// target_id: string;
// }
// | {
// type: 'emoji_reaction';
// emoji: string;
// emoji_url: string | null;
// status_id: string;
// }
// | {
// type: 'chat_mention';
// chat_message_id: string;
// }
// | {
// type: 'participation_accepted' | 'participation_request';
// status_id: string;
// participation_message: string | null;
// }
// ) = {
// ...omit(notification, ['account', 'accounts']),
// created_at: notification.latest_page_notification_at,
// id: notification.id,
// type: notification.type,
// };
// @ts-ignore
if (notification.status) minifiedNotification.status_id = notification.status.id;
// @ts-ignore
if (notification.target) minifiedNotification.target_id = notification.target.id;
// @ts-ignore
if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id;
// // @ts-ignore
// if (notification.status) minifiedNotification.status_id = notification.status.id;
// // @ts-ignore
// if (notification.target) minifiedNotification.target_id = notification.target.id;
// // @ts-ignore
// if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id;
return minifiedNotification;
};
// return minifiedNotification;
// };
type MinifiedNotification = ReturnType<typeof minifyNotification>;
// type MinifiedNotification = ReturnType<typeof minifyNotification>;
// Count how many notifications appear after the given ID (for unread count)
const countFuture = (notifications: ImmutableOrderedMap<string, MinifiedNotification>, lastId: string | number) =>
const countFuture = (notifications: ImmutableOrderedMap<string, NotificationGroup>, lastId: string | number) =>
notifications.reduce((acc, notification) => {
if (!notification.duplicate && parseId(notification.id) > parseId(lastId)) {
if (parseId(notification.group_key) > parseId(lastId)) {
return acc + 1;
} else {
return acc;
}
}, 0);
const importNotification = (state: State, notification: Notification) => {
const importNotification = (state: State, notification: NotificationGroup) => {
const top = state.top;
if (!top && !notification.duplicate) state = state.update('unread', unread => unread + 1);
if (!top) state = state.update('unread', unread => unread + 1);
return state.update('items', map => {
if (top && map.size > 40) {
map = map.take(20);
}
return map.set(notification.id, minifyNotification(notification)).sort(comparator);
return map.set(notification.group_key, notification).sort(comparator);
});
};
const expandNormalizedNotifications = (state: State, notifications: Notification[], next: (() => Promise<PaginatedResponse<BaseNotification>>) | null) => {
const items = ImmutableOrderedMap(notifications.map(minifyNotification).map(n => [n.id, n]));
const expandNormalizedNotifications = (state: State, notifications: NotificationGroup[], next: (() => Promise<PaginatedResponse<BaseNotification>>) | null) => {
const items = ImmutableOrderedMap(notifications.map(n => [n.group_key, n]));
return state.withMutations(mutable => {
mutable.update('items', map => map.merge(items).sort(comparator));
@ -161,10 +159,10 @@ const expandNormalizedNotifications = (state: State, notifications: Notification
};
const filterNotifications = (state: State, relationship: Relationship) =>
state.update('items', map => map.filterNot(item => item !== null && item.account_ids.includes(relationship.id)));
state.update('items', map => map.filterNot(item => item !== null && item.sample_account_ids.includes(relationship.id)));
const filterNotificationIds = (state: State, accountIds: Array<string>, type?: string) => {
const helper = (list: ImmutableOrderedMap<string, MinifiedNotification>) => list.filterNot(item => item !== null && accountIds.includes(item.account_ids[0]) && (type === undefined || type === item.type));
const helper = (list: ImmutableOrderedMap<string, NotificationGroup>) => list.filterNot(item => item !== null && accountIds.includes(item.sample_account_ids[0]) && (type === undefined || type === item.type));
return state.update('items', helper);
};
@ -182,14 +180,14 @@ const updateNotificationsQueue = (state: State, notification: BaseNotification,
const listedNotifications = state.items;
const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount;
const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id);
const alreadyExists = queuedNotifications.has(notification.group_key) || listedNotifications.has(notification.group_key);
if (alreadyExists) return state;
const newQueuedNotifications = queuedNotifications;
return state.withMutations(mutable => {
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, QueuedNotificationRecord({
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.group_key, QueuedNotificationRecord({
notification,
intlMessages,
intlLocale,
@ -259,7 +257,4 @@ const notifications = (state: State = ReducerRecord(), action: AnyAction | Timel
}
};
export {
notifications as default,
type MinifiedNotification,
};
export { notifications as default };

View file

@ -13,12 +13,10 @@ import { validId } from 'pl-fe/utils/auth';
import ConfigDB from 'pl-fe/utils/config-db';
import { shouldFilter } from 'pl-fe/utils/timelines';
import type { Account as BaseAccount, Filter, MediaAttachment, Relationship } from 'pl-api';
import type { Account as BaseAccount, Filter, MediaAttachment, NotificationGroup, Relationship } from 'pl-api';
import type { EntityStore } from 'pl-fe/entity-store/types';
import type { Account } from 'pl-fe/normalizers/account';
import type { Group } from 'pl-fe/normalizers/group';
import type { Notification } from 'pl-fe/normalizers/notification';
import type { MinifiedNotification } from 'pl-fe/reducers/notifications';
import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
import type { MRFSimple } from 'pl-fe/schemas/pleroma';
import type { RootState } from 'pl-fe/store';
@ -27,7 +25,9 @@ const selectAccount = (state: RootState, accountId: string) =>
state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined;
const selectAccounts = (state: RootState, accountIds: Array<string>) =>
accountIds.map(accountId => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined);
accountIds
.map(accountId => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined)
.filter((account): account is Account => account !== undefined);
const selectOwnAccount = (state: RootState) => {
if (state.me) {
@ -177,26 +177,33 @@ const makeGetStatus = () => createSelector(
type SelectedStatus = Exclude<ReturnType<ReturnType<typeof makeGetStatus>>, null>;
const makeGetNotification = () => createSelector([
(_state: RootState, notification: MinifiedNotification) => notification,
(_state: RootState, notification: NotificationGroup) => notification,
// @ts-ignore
(state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.account_id),
(state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id),
// @ts-ignore
(state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.target_id),
// @ts-ignore
(state: RootState, notification: MinifiedNotification) => state.statuses.get(notification.status_id),
(state: RootState, notification: MinifiedNotification) => notification.account_ids ? selectAccounts(state, notification.account_ids) : null,
], (notification, account, target, status, accounts): MinifiedNotification & Notification => ({
(state: RootState, notification: NotificationGroup) => state.statuses.get(notification.status_id),
(state: RootState, notification: NotificationGroup) => selectAccounts(state, notification.sample_account_ids),
], (notification, target, status, accounts): SelectedNotification => ({
...notification,
// @ts-ignore
account: account || null,
// @ts-ignore
target: target || null,
// @ts-ignore
status: status || null,
target,
// @ts-ignore
status,
accounts,
}));
type SelectedNotification = NotificationGroup & {
accounts: Array<Account>;
} & ({
type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite';
} | {
type: 'status' | 'mention' | 'reblog' | 'favourite' | 'poll' | 'update' | 'emoji_reaction' | 'event_reminder' | 'participation_accepted' | 'participation_request';
status: MinifiedStatus;
} | {
type: 'move';
target: Account;
})
type AccountGalleryAttachment = MediaAttachment & {
status: MinifiedStatus;
account: BaseAccount;
@ -357,6 +364,7 @@ export {
makeGetStatus,
type SelectedStatus,
makeGetNotification,
type SelectedNotification,
type AccountGalleryAttachment,
getAccountGallery,
getGroupGallery,

View file

@ -6651,6 +6651,11 @@ lodash.merge@^4.6.0, lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.omit@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@ -7570,13 +7575,14 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
pl-api@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.5.tgz#373d58fc40ae23b12c6d20def8d3332112e2dd93"
integrity sha512-IMcwANPtTRMv+tTLW4ic/o/usYIHeapR+aOx5YhmmVQsXTiVUZQtCVLmrfIrqMnyXm8PHvE/arzzozRFU8+GXQ==
pl-api@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.7.tgz#929b557903b280bda9e7c7bf6aa8cd015698c42f"
integrity sha512-cTUHacufHDlNl7zHsOH+LW5MvotY5pUnBhfFMofVfvGOswURl0F1x1L8+V5k1D4VGn+mE7CnOv3esz/3QrrQmg==
dependencies:
blurhash "^2.0.5"
http-link-header "^1.1.3"
lodash.omit "^4.5.0"
lodash.pick "^4.4.0"
object-to-formdata "^4.5.1"
query-string "^9.1.0"