pl-api: Start implemenitng grouped notifications with fallback to v1
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
199c43d241
commit
94b3af03e9
10 changed files with 128 additions and 19 deletions
|
@ -1,3 +1,4 @@
|
|||
import omit from 'lodash.omit';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import {
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
contextSchema,
|
||||
conversationSchema,
|
||||
credentialAccountSchema,
|
||||
credentialApplicationSchema,
|
||||
customEmojiSchema,
|
||||
domainBlockSchema,
|
||||
emojiReactionSchema,
|
||||
|
@ -70,6 +72,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 +110,7 @@ import {
|
|||
MuteAccountParams,
|
||||
UpdateFilterParams,
|
||||
} from './params/filtering';
|
||||
import { GetGroupedNotificationsParams } from './params/grouped-notifications';
|
||||
import {
|
||||
CreateGroupParams,
|
||||
GetGroupBlocksParams,
|
||||
|
@ -195,6 +199,7 @@ import type {
|
|||
AdminReport,
|
||||
GroupRole,
|
||||
Instance,
|
||||
Notification,
|
||||
PleromaConfig,
|
||||
Status,
|
||||
StreamingEvent,
|
||||
|
@ -228,7 +233,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
|
||||
|
@ -353,6 +360,60 @@ 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: items.map(({ account }) => account),
|
||||
statuses: items.reduce<Array<Status>>((statuses, notification) => {
|
||||
if ('status' in notification) statuses.push(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 +424,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 +2766,16 @@ class PlApiClient {
|
|||
|
||||
};
|
||||
|
||||
public readonly groupedNotifications = {
|
||||
getGroupedNotifications: async (params: GetGroupedNotificationsParams) => {
|
||||
if (this.features.groupedNotifications) {
|
||||
return this.#paginatedGet('/api/v2/notifications', { params }, groupedNotificationsResultsSchema);
|
||||
} else {
|
||||
return this.#groupNotifications(await this.notifications.getNotifications(), params);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
public readonly pushNotifications = {
|
||||
/**
|
||||
* Subscribe to push notifications
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue