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 * as v from 'valibot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +28,7 @@ import {
|
||||||
contextSchema,
|
contextSchema,
|
||||||
conversationSchema,
|
conversationSchema,
|
||||||
credentialAccountSchema,
|
credentialAccountSchema,
|
||||||
|
credentialApplicationSchema,
|
||||||
customEmojiSchema,
|
customEmojiSchema,
|
||||||
domainBlockSchema,
|
domainBlockSchema,
|
||||||
emojiReactionSchema,
|
emojiReactionSchema,
|
||||||
|
@ -70,6 +72,7 @@ import {
|
||||||
trendsLinkSchema,
|
trendsLinkSchema,
|
||||||
webPushSubscriptionSchema,
|
webPushSubscriptionSchema,
|
||||||
} from './entities';
|
} from './entities';
|
||||||
|
import { GroupedNotificationsResults, groupedNotificationsResultsSchema, NotificationGroup } from './entities/grouped-notifications-results';
|
||||||
import { filteredArray } from './entities/utils';
|
import { filteredArray } from './entities/utils';
|
||||||
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA } from './features';
|
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA } from './features';
|
||||||
import {
|
import {
|
||||||
|
@ -107,6 +110,7 @@ import {
|
||||||
MuteAccountParams,
|
MuteAccountParams,
|
||||||
UpdateFilterParams,
|
UpdateFilterParams,
|
||||||
} from './params/filtering';
|
} from './params/filtering';
|
||||||
|
import { GetGroupedNotificationsParams } from './params/grouped-notifications';
|
||||||
import {
|
import {
|
||||||
CreateGroupParams,
|
CreateGroupParams,
|
||||||
GetGroupBlocksParams,
|
GetGroupBlocksParams,
|
||||||
|
@ -195,6 +199,7 @@ import type {
|
||||||
AdminReport,
|
AdminReport,
|
||||||
GroupRole,
|
GroupRole,
|
||||||
Instance,
|
Instance,
|
||||||
|
Notification,
|
||||||
PleromaConfig,
|
PleromaConfig,
|
||||||
Status,
|
Status,
|
||||||
StreamingEvent,
|
StreamingEvent,
|
||||||
|
@ -228,7 +233,9 @@ import type {
|
||||||
AdminUpdateRuleParams,
|
AdminUpdateRuleParams,
|
||||||
AdminUpdateStatusParams,
|
AdminUpdateStatusParams,
|
||||||
} from './params/admin';
|
} 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
|
* @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. */
|
/** Register client applications that can be used to obtain OAuth tokens. */
|
||||||
public readonly apps = {
|
public readonly apps = {
|
||||||
/**
|
/**
|
||||||
|
@ -363,7 +424,7 @@ class PlApiClient {
|
||||||
createApplication: async (params: CreateApplicationParams) => {
|
createApplication: async (params: CreateApplicationParams) => {
|
||||||
const response = await this.request('/api/v1/apps', { method: 'POST', body: params });
|
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 = {
|
public readonly pushNotifications = {
|
||||||
/**
|
/**
|
||||||
* Subscribe to push notifications
|
* Subscribe to push notifications
|
||||||
|
|
|
@ -12,9 +12,6 @@ const applicationSchema = v.pipe(v.any(), v.transform((application) => ({
|
||||||
})), v.object({
|
})), v.object({
|
||||||
name: v.fallback(v.string(), ''),
|
name: v.fallback(v.string(), ''),
|
||||||
website: v.fallback(v.optional(v.string()), undefined),
|
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()),
|
redirect_uris: filteredArray(v.string()),
|
||||||
|
|
||||||
id: v.fallback(v.optional(v.string()), undefined),
|
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>;
|
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,
|
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.
|
* Groups.
|
||||||
* @see POST /api/v1/groups
|
* @see POST /api/v1/groups
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
interface PaginatedResponse<T> {
|
interface PaginatedSingleResponse<T> {
|
||||||
previous: (() => Promise<PaginatedResponse<T>>) | null;
|
previous: (() => Promise<PaginatedSingleResponse<T>>) | null;
|
||||||
next: (() => Promise<PaginatedResponse<T>>) | null;
|
next: (() => Promise<PaginatedSingleResponse<T>>) | null;
|
||||||
items: Array<T>;
|
items: T;
|
||||||
partial: boolean;
|
partial: boolean;
|
||||||
total?: number;
|
total?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaginatedResponse<T> = PaginatedSingleResponse<Array<T>>;
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
PaginatedSingleResponse,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^2.8.0",
|
"@stylistic/eslint-plugin": "^2.8.0",
|
||||||
"@types/http-link-header": "^1.0.7",
|
"@types/http-link-header": "^1.0.7",
|
||||||
|
"@types/lodash.omit": "^4.5.9",
|
||||||
"@types/lodash.pick": "^4.4.9",
|
"@types/lodash.pick": "^4.4.9",
|
||||||
"@types/node": "^22.7.4",
|
"@types/node": "^22.7.4",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"http-link-header": "^1.1.3",
|
"http-link-header": "^1.1.3",
|
||||||
|
"lodash.omit": "^4.5.0",
|
||||||
"lodash.pick": "^4.4.0",
|
"lodash.pick": "^4.4.0",
|
||||||
"object-to-formdata": "^4.5.1",
|
"object-to-formdata": "^4.5.1",
|
||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
|
|
|
@ -478,6 +478,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
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":
|
"@types/lodash.pick@^4.4.9":
|
||||||
version "4.4.9"
|
version "4.4.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.9.tgz#06f7d88faa81a6c5665584778aea7b1374a1dc5b"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
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:
|
lodash.pick@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
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);
|
const scopes = getInstanceScopes(instance);
|
||||||
|
|
||||||
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
|
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({
|
const query = new URLSearchParams({
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri,
|
redirect_uri: redirect_uri || app.redirect_uris[0]!,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
scope: scopes,
|
scope: scopes,
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,8 +13,6 @@ import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||||
|
|
||||||
const FE_NAME = 'pl_fe';
|
const FE_NAME = 'pl_fe';
|
||||||
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
/** Options when changing/saving settings. */
|
/** Options when changing/saving settings. */
|
||||||
type SettingOpts = {
|
type SettingOpts = {
|
||||||
/** Whether to display an alert when settings are saved. */
|
/** Whether to display an alert when settings are saved. */
|
||||||
|
@ -71,7 +69,7 @@ const updateSettingsStore = (settings: any) =>
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const accountUrl = getAccount(state, state.me as string)!.url;
|
const accountUrl = makeGetAccount()(state, state.me as string)!.url;
|
||||||
|
|
||||||
return updateAuthAccount(accountUrl, settings);
|
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 { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||||
import { getBaseURL } from 'pl-fe/utils/accounts';
|
import { getBaseURL } from 'pl-fe/utils/accounts';
|
||||||
|
|
||||||
import type { Token } from 'pl-api';
|
import type { CredentialApplication, Token } from 'pl-api';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.app_create', defaultMessage: 'Create app' },
|
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 baseURL = getBaseURL(account!);
|
||||||
|
|
||||||
const tokenParams = {
|
const tokenParams = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||||
import trim from 'lodash/trim';
|
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 * as v from 'valibot';
|
||||||
|
|
||||||
import { MASTODON_PRELOAD_IMPORT, type PreloadAction } from 'pl-fe/actions/preload';
|
import { MASTODON_PRELOAD_IMPORT, type PreloadAction } from 'pl-fe/actions/preload';
|
||||||
|
@ -33,7 +33,7 @@ const AuthUserRecord = ImmutableRecord({
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
const ReducerRecord = ImmutableRecord({
|
||||||
app: null as Application | null,
|
app: null as CredentialApplication | null,
|
||||||
tokens: ImmutableMap<string, Token>(),
|
tokens: ImmutableMap<string, Token>(),
|
||||||
users: ImmutableMap<string, AuthUser>(),
|
users: ImmutableMap<string, AuthUser>(),
|
||||||
me: null as string | null,
|
me: null as string | null,
|
||||||
|
|
Loading…
Reference in a new issue