Merge pull request #141 from mkljczk/pl-api-updates
This commit is contained in:
commit
d0a036c63c
22 changed files with 642 additions and 235 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
import pick from 'lodash.pick';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +29,7 @@ import {
|
||||||
contextSchema,
|
contextSchema,
|
||||||
conversationSchema,
|
conversationSchema,
|
||||||
credentialAccountSchema,
|
credentialAccountSchema,
|
||||||
|
credentialApplicationSchema,
|
||||||
customEmojiSchema,
|
customEmojiSchema,
|
||||||
domainBlockSchema,
|
domainBlockSchema,
|
||||||
emojiReactionSchema,
|
emojiReactionSchema,
|
||||||
|
@ -70,6 +73,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 +111,7 @@ import {
|
||||||
MuteAccountParams,
|
MuteAccountParams,
|
||||||
UpdateFilterParams,
|
UpdateFilterParams,
|
||||||
} from './params/filtering';
|
} from './params/filtering';
|
||||||
|
import { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams } from './params/grouped-notifications';
|
||||||
import {
|
import {
|
||||||
CreateGroupParams,
|
CreateGroupParams,
|
||||||
GetGroupBlocksParams,
|
GetGroupBlocksParams,
|
||||||
|
@ -189,12 +194,14 @@ import request, { getNextLink, getPrevLink, type RequestBody, RequestMeta } from
|
||||||
import { buildFullPath } from './utils/url';
|
import { buildFullPath } from './utils/url';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Account,
|
||||||
AdminAccount,
|
AdminAccount,
|
||||||
AdminAnnouncement,
|
AdminAnnouncement,
|
||||||
AdminModerationLogEntry,
|
AdminModerationLogEntry,
|
||||||
AdminReport,
|
AdminReport,
|
||||||
GroupRole,
|
GroupRole,
|
||||||
Instance,
|
Instance,
|
||||||
|
Notification,
|
||||||
PleromaConfig,
|
PleromaConfig,
|
||||||
Status,
|
Status,
|
||||||
StreamingEvent,
|
StreamingEvent,
|
||||||
|
@ -228,7 +235,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
|
||||||
|
@ -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: {
|
#paginatedPleromaAccounts = async (params: {
|
||||||
query?: string;
|
query?: string;
|
||||||
filters?: 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. */
|
/** Register client applications that can be used to obtain OAuth tokens. */
|
||||||
public readonly apps = {
|
public readonly apps = {
|
||||||
/**
|
/**
|
||||||
|
@ -363,7 +453,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 +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 = {
|
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 };
|
||||||
|
|
151
packages/pl-api/lib/entities/grouped-notifications-results.ts
Normal file
151
packages/pl-api/lib/entities/grouped-notifications-results.ts
Normal 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 };
|
|
@ -41,6 +41,7 @@ export * from './filter';
|
||||||
export * from './group';
|
export * from './group';
|
||||||
export * from './group-member';
|
export * from './group-member';
|
||||||
export * from './group-relationship';
|
export * from './group-relationship';
|
||||||
|
export * from './grouped-notifications-results';
|
||||||
export * from './instance';
|
export * from './instance';
|
||||||
export * from './interaction-policy';
|
export * from './interaction-policy';
|
||||||
export * from './interaction-request';
|
export * from './interaction-request';
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
packages/pl-api/lib/params/grouped-notifications.ts
Normal file
31
packages/pl-api/lib/params/grouped-notifications.ts
Normal 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 user’s 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 };
|
|
@ -4,6 +4,7 @@ export * from './apps';
|
||||||
export * from './chats';
|
export * from './chats';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './filtering';
|
export * from './filtering';
|
||||||
|
export * from './grouped-notifications';
|
||||||
export * from './groups';
|
export * from './groups';
|
||||||
export * from './instance';
|
export * from './instance';
|
||||||
export * from './interaction-requests';
|
export * from './interaction-requests';
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pl-api",
|
"name": "pl-api",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
|
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
"mini-css-extract-plugin": "^2.9.1",
|
"mini-css-extract-plugin": "^2.9.1",
|
||||||
"multiselect-react-dropdown": "^2.0.25",
|
"multiselect-react-dropdown": "^2.0.25",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pl-api": "^0.1.5",
|
"pl-api": "^0.1.7",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { getClient } from 'pl-fe/api';
|
import { getClient } from 'pl-fe/api';
|
||||||
import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification';
|
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 { getFilters, regexFromFilters } from 'pl-fe/selectors';
|
||||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||||
import { isLoggedIn } from 'pl-fe/utils/auth';
|
import { isLoggedIn } from 'pl-fe/utils/auth';
|
||||||
|
@ -18,7 +18,7 @@ import { importEntities } from './importer';
|
||||||
import { saveMarker } from './markers';
|
import { saveMarker } from './markers';
|
||||||
import { saveSettings } from './settings';
|
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';
|
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||||
|
|
||||||
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const;
|
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const;
|
||||||
|
@ -58,8 +58,8 @@ defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array<BaseNotification>) => {
|
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array<NotificationGroup>) => {
|
||||||
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
|
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.sample_account_ids).flat();
|
||||||
|
|
||||||
if (accountIds.length > 0) {
|
if (accountIds.length > 0) {
|
||||||
dispatch(fetchRelationships(accountIds));
|
dispatch(fetchRelationships(accountIds));
|
||||||
|
@ -76,13 +76,16 @@ const updateNotifications = (notification: BaseNotification) =>
|
||||||
statuses: [getNotificationStatus(notification)],
|
statuses: [getNotificationStatus(notification)],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
if (showInColumn) {
|
if (showInColumn) {
|
||||||
|
const normalizedNotification = normalizeNotification(notification);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
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,
|
max_id: maxId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -203,7 +206,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
if (features.notificationsIncludeTypes) {
|
if (features.notificationsIncludeTypes) {
|
||||||
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
|
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
|
||||||
} else {
|
} else {
|
||||||
params.exclude_types = EXCLUDE_TYPES;
|
params.exclude_types = [...EXCLUDE_TYPES];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const filtered = FILTER_TYPES[activeFilter] || [activeFilter];
|
const filtered = FILTER_TYPES[activeFilter] || [activeFilter];
|
||||||
|
@ -215,51 +218,65 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!maxId && notifications.items.size > 0) {
|
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());
|
dispatch(expandNotificationsRequest());
|
||||||
|
|
||||||
return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => {
|
return getClient(state).groupedNotifications.getGroupedNotifications(params, { signal: abortExpandNotifications.signal }).then(({ items: { accounts, statuses, notification_groups }, next }) => {
|
||||||
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({
|
dispatch(importEntities({
|
||||||
accounts: Object.values(entries.accounts),
|
accounts,
|
||||||
statuses: Object.values(entries.statuses),
|
statuses,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items);
|
dispatch(expandNotificationsSuccess(notification_groups, next));
|
||||||
|
fetchRelatedRelationships(dispatch, notification_groups);
|
||||||
dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next));
|
|
||||||
fetchRelatedRelationships(dispatch, response.items);
|
|
||||||
done();
|
done();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandNotificationsFail(error));
|
dispatch(expandNotificationsFail(error));
|
||||||
done();
|
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 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,
|
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
notifications,
|
notifications,
|
||||||
next,
|
next,
|
||||||
|
@ -297,7 +314,7 @@ const markReadNotifications = () =>
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const topNotificationId = state.notifications.items.first()?.id;
|
const topNotificationId = state.notifications.items.first()?.page_max_id;
|
||||||
const lastReadId = state.notifications.lastRead;
|
const lastReadId = state.notifications.lastRead;
|
||||||
|
|
||||||
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
|
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -24,11 +24,9 @@ import { useModalsStore } from 'pl-fe/stores/modals';
|
||||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||||
import { NotificationType } from 'pl-fe/utils/notification';
|
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 { 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 { 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 notificationForScreenReader = (intl: IntlShape, message: string, timestamp: string) => {
|
||||||
const output = [message];
|
const output = [message];
|
||||||
|
@ -184,13 +182,13 @@ const avatarSize = 48;
|
||||||
|
|
||||||
interface INotification {
|
interface INotification {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
notification: MinifiedNotification;
|
notification: NotificationGroup;
|
||||||
onMoveUp?: (notificationId: string) => void;
|
onMoveUp?: (notificationId: string) => void;
|
||||||
onMoveDown?: (notificationId: string) => void;
|
onMoveDown?: (notificationId: string) => void;
|
||||||
onReblog?: (status: StatusEntity, e?: KeyboardEvent) => 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))
|
if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return n.status;
|
return n.status;
|
||||||
|
@ -207,15 +205,17 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
const { me } = useLoggedIn();
|
const { me } = useLoggedIn();
|
||||||
const { openModal } = useModalsStore();
|
const { openModal } = useModalsStore();
|
||||||
const { settings } = useSettingsStore();
|
const { settings } = useSettingsStore();
|
||||||
|
|
||||||
const notification = useAppSelector((state) => getNotification(state, props.notification));
|
const notification = useAppSelector((state) => getNotification(state, props.notification));
|
||||||
|
const status = getNotificationStatus(notification);
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
|
|
||||||
const type = notification.type;
|
const type = notification.type;
|
||||||
const { account, accounts } = notification;
|
const { accounts } = notification;
|
||||||
const status = getNotificationStatus(notification);
|
const account = accounts[0];
|
||||||
|
|
||||||
const getHandlers = () => ({
|
const getHandlers = () => ({
|
||||||
reply: handleMention,
|
reply: handleMention,
|
||||||
|
@ -289,13 +289,13 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
|
|
||||||
const handleMoveUp = () => {
|
const handleMoveUp = () => {
|
||||||
if (onMoveUp) {
|
if (onMoveUp) {
|
||||||
onMoveUp(notification.id);
|
onMoveUp(notification.group_key);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveDown = () => {
|
const handleMoveDown = () => {
|
||||||
if (onMoveDown) {
|
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 : '',
|
name: account && typeof account === 'object' ? account.acct : '',
|
||||||
targetName,
|
targetName,
|
||||||
}),
|
}),
|
||||||
notification.created_at,
|
notification.latest_page_notification_at!,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -433,7 +433,7 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
truncate
|
truncate
|
||||||
data-testid='message'
|
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>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -32,7 +32,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const getNotifications = createSelector([
|
const getNotifications = createSelector([
|
||||||
(state: RootState) => state.notifications.items.toList(),
|
(state: RootState) => state.notifications.items.toList(),
|
||||||
], (notifications) => notifications.filter(item => item !== null && !item.duplicate));
|
], (notifications) => notifications.filter(item => item !== null));
|
||||||
|
|
||||||
const Notifications = () => {
|
const Notifications = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -54,8 +54,13 @@ const Notifications = () => {
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const handleLoadOlder = useCallback(debounce(() => {
|
const handleLoadOlder = useCallback(debounce(() => {
|
||||||
const last = notifications.last();
|
const minId = notifications.reduce<string | undefined>(
|
||||||
dispatch(expandNotifications({ maxId: last && last.id }));
|
(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]);
|
}, 300, { leading: true }), [notifications]);
|
||||||
|
|
||||||
const handleScroll = useCallback(debounce((startIndex?: number) => {
|
const handleScroll = useCallback(debounce((startIndex?: number) => {
|
||||||
|
@ -63,12 +68,12 @@ const Notifications = () => {
|
||||||
}, 100), []);
|
}, 100), []);
|
||||||
|
|
||||||
const handleMoveUp = (id: string) => {
|
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);
|
_selectChild(elementIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveDown = (id: string) => {
|
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);
|
_selectChild(elementIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,7 +116,7 @@ const Notifications = () => {
|
||||||
} else if (notifications.size > 0 || hasMore) {
|
} else if (notifications.size > 0 || hasMore) {
|
||||||
scrollableContent = notifications.map((item) => (
|
scrollableContent = notifications.map((item) => (
|
||||||
<Notification
|
<Notification
|
||||||
key={item.id}
|
key={item.group_key}
|
||||||
notification={item}
|
notification={item}
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
|
|
|
@ -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';
|
const normalizeNotification = (notification: BaseNotification): NotificationGroup => ({
|
||||||
import type { Notification as BaseNotification } from 'pl-api';
|
...(omit(notification, ['account', 'status', 'target'])),
|
||||||
import type { MinifiedNotification } from 'pl-fe/reducers/notifications';
|
group_key: notification.id,
|
||||||
|
notifications_count: 1,
|
||||||
const STATUS_NOTIFICATION_TYPES = [
|
most_recent_notification_id: notification.id,
|
||||||
'favourite',
|
page_min_id: notification.id,
|
||||||
'reblog',
|
page_max_id: notification.id,
|
||||||
'emoji_reaction',
|
latest_page_notification_at: notification.created_at,
|
||||||
'event_reminder',
|
sample_account_ids: [notification.account.id],
|
||||||
'participation_accepted',
|
// @ts-ignore
|
||||||
'participation_request',
|
status_id: notification.status?.id,
|
||||||
];
|
// @ts-ignore
|
||||||
|
target_id: notification.target?.id,
|
||||||
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 normalizeNotifications = (notifications: Array<BaseNotification>, stateNotifications?: ImmutableOrderedMap<string, MinifiedNotification>) => {
|
export { normalizeNotification };
|
||||||
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 };
|
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -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 persistSession = (state: State) => {
|
||||||
const me = state.me;
|
const me = state.me;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Record as ImmutableRecord, OrderedMap as ImmutableOrderedMap } from 'immutable';
|
import { Record as ImmutableRecord, OrderedMap as ImmutableOrderedMap } from 'immutable';
|
||||||
import omit from 'lodash/omit';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
@ -27,8 +26,7 @@ import {
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
|
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 as BaseNotification, Markers, NotificationGroup, PaginatedResponse, Relationship } from 'pl-api';
|
||||||
import type { Notification } from 'pl-fe/normalizers/notification';
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
|
||||||
const QueuedNotificationRecord = ImmutableRecord({
|
const QueuedNotificationRecord = ImmutableRecord({
|
||||||
|
@ -38,7 +36,7 @@ const QueuedNotificationRecord = ImmutableRecord({
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
const ReducerRecord = ImmutableRecord({
|
||||||
items: ImmutableOrderedMap<string, MinifiedNotification>(),
|
items: ImmutableOrderedMap<string, NotificationGroup>(),
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
top: false,
|
top: false,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
|
@ -54,103 +52,103 @@ type QueuedNotification = ReturnType<typeof QueuedNotificationRecord>;
|
||||||
const parseId = (id: string | number) => parseInt(id as string, 10);
|
const parseId = (id: string | number) => parseInt(id as string, 10);
|
||||||
|
|
||||||
// For sorting the notifications
|
// For sorting the notifications
|
||||||
const comparator = (a: Pick<Notification, 'id'>, b: Pick<Notification, 'id'>) => {
|
const comparator = (a: Pick<NotificationGroup, 'group_key'>, b: Pick<NotificationGroup, 'group_key'>) => {
|
||||||
const parse = (m: Pick<Notification, 'id'>) => parseId(m.id);
|
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;
|
||||||
if (parse(a) > parse(b)) return -1;
|
if (parse(a) > parse(b)) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const minifyNotification = (notification: Notification) => {
|
// const minifyNotification = (notification: Notification) => {
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
const minifiedNotification: {
|
// const minifiedNotification: {
|
||||||
duplicate: boolean;
|
// duplicate: boolean;
|
||||||
account_id: string;
|
// account_id: string;
|
||||||
account_ids: string[];
|
// account_ids: string[];
|
||||||
created_at: string;
|
// created_at: string;
|
||||||
id: string;
|
// id: string;
|
||||||
} & (
|
// } & (
|
||||||
| { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
|
// | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
|
||||||
| {
|
// | {
|
||||||
type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
|
// type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
|
||||||
status_id: string;
|
// status_id: string;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'admin.report';
|
// type: 'admin.report';
|
||||||
report: Report;
|
// report: Report;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'severed_relationships';
|
// type: 'severed_relationships';
|
||||||
relationship_severance_event: RelationshipSeveranceEvent;
|
// relationship_severance_event: RelationshipSeveranceEvent;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'moderation_warning';
|
// type: 'moderation_warning';
|
||||||
moderation_warning: AccountWarning;
|
// moderation_warning: AccountWarning;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'move';
|
// type: 'move';
|
||||||
target_id: string;
|
// target_id: string;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'emoji_reaction';
|
// type: 'emoji_reaction';
|
||||||
emoji: string;
|
// emoji: string;
|
||||||
emoji_url: string | null;
|
// emoji_url: string | null;
|
||||||
status_id: string;
|
// status_id: string;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'chat_mention';
|
// type: 'chat_mention';
|
||||||
chat_message_id: string;
|
// chat_message_id: string;
|
||||||
}
|
// }
|
||||||
| {
|
// | {
|
||||||
type: 'participation_accepted' | 'participation_request';
|
// type: 'participation_accepted' | 'participation_request';
|
||||||
status_id: string;
|
// status_id: string;
|
||||||
participation_message: string | null;
|
// participation_message: string | null;
|
||||||
}
|
// }
|
||||||
) = {
|
// ) = {
|
||||||
...omit(notification, ['account', 'accounts']),
|
// ...omit(notification, ['account', 'accounts']),
|
||||||
created_at: notification.created_at,
|
// created_at: notification.latest_page_notification_at,
|
||||||
id: notification.id,
|
// id: notification.id,
|
||||||
type: notification.type,
|
// type: notification.type,
|
||||||
};
|
// };
|
||||||
|
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
if (notification.status) minifiedNotification.status_id = notification.status.id;
|
// if (notification.status) minifiedNotification.status_id = notification.status.id;
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
if (notification.target) minifiedNotification.target_id = notification.target.id;
|
// if (notification.target) minifiedNotification.target_id = notification.target.id;
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id;
|
// 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)
|
// 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) => {
|
notifications.reduce((acc, notification) => {
|
||||||
if (!notification.duplicate && parseId(notification.id) > parseId(lastId)) {
|
if (parseId(notification.group_key) > parseId(lastId)) {
|
||||||
return acc + 1;
|
return acc + 1;
|
||||||
} else {
|
} else {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const importNotification = (state: State, notification: Notification) => {
|
const importNotification = (state: State, notification: NotificationGroup) => {
|
||||||
const top = state.top;
|
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 => {
|
return state.update('items', map => {
|
||||||
if (top && map.size > 40) {
|
if (top && map.size > 40) {
|
||||||
map = map.take(20);
|
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 expandNormalizedNotifications = (state: State, notifications: NotificationGroup[], next: (() => Promise<PaginatedResponse<BaseNotification>>) | null) => {
|
||||||
const items = ImmutableOrderedMap(notifications.map(minifyNotification).map(n => [n.id, n]));
|
const items = ImmutableOrderedMap(notifications.map(n => [n.group_key, n]));
|
||||||
|
|
||||||
return state.withMutations(mutable => {
|
return state.withMutations(mutable => {
|
||||||
mutable.update('items', map => map.merge(items).sort(comparator));
|
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) =>
|
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 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);
|
return state.update('items', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -182,14 +180,14 @@ const updateNotificationsQueue = (state: State, notification: BaseNotification,
|
||||||
const listedNotifications = state.items;
|
const listedNotifications = state.items;
|
||||||
const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount;
|
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;
|
if (alreadyExists) return state;
|
||||||
|
|
||||||
const newQueuedNotifications = queuedNotifications;
|
const newQueuedNotifications = queuedNotifications;
|
||||||
|
|
||||||
return state.withMutations(mutable => {
|
return state.withMutations(mutable => {
|
||||||
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||||
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, QueuedNotificationRecord({
|
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.group_key, QueuedNotificationRecord({
|
||||||
notification,
|
notification,
|
||||||
intlMessages,
|
intlMessages,
|
||||||
intlLocale,
|
intlLocale,
|
||||||
|
@ -259,7 +257,4 @@ const notifications = (state: State = ReducerRecord(), action: AnyAction | Timel
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { notifications as default };
|
||||||
notifications as default,
|
|
||||||
type MinifiedNotification,
|
|
||||||
};
|
|
||||||
|
|
|
@ -13,12 +13,10 @@ import { validId } from 'pl-fe/utils/auth';
|
||||||
import ConfigDB from 'pl-fe/utils/config-db';
|
import ConfigDB from 'pl-fe/utils/config-db';
|
||||||
import { shouldFilter } from 'pl-fe/utils/timelines';
|
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 { EntityStore } from 'pl-fe/entity-store/types';
|
||||||
import type { Account } from 'pl-fe/normalizers/account';
|
import type { Account } from 'pl-fe/normalizers/account';
|
||||||
import type { Group } from 'pl-fe/normalizers/group';
|
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 { MinifiedStatus } from 'pl-fe/reducers/statuses';
|
||||||
import type { MRFSimple } from 'pl-fe/schemas/pleroma';
|
import type { MRFSimple } from 'pl-fe/schemas/pleroma';
|
||||||
import type { RootState } from 'pl-fe/store';
|
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;
|
state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined;
|
||||||
|
|
||||||
const selectAccounts = (state: RootState, accountIds: Array<string>) =>
|
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) => {
|
const selectOwnAccount = (state: RootState) => {
|
||||||
if (state.me) {
|
if (state.me) {
|
||||||
|
@ -177,26 +177,33 @@ const makeGetStatus = () => createSelector(
|
||||||
type SelectedStatus = Exclude<ReturnType<ReturnType<typeof makeGetStatus>>, null>;
|
type SelectedStatus = Exclude<ReturnType<ReturnType<typeof makeGetStatus>>, null>;
|
||||||
|
|
||||||
const makeGetNotification = () => createSelector([
|
const makeGetNotification = () => createSelector([
|
||||||
(_state: RootState, notification: MinifiedNotification) => notification,
|
(_state: RootState, notification: NotificationGroup) => notification,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.account_id),
|
(state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.target_id),
|
(state: RootState, notification: NotificationGroup) => state.statuses.get(notification.status_id),
|
||||||
// @ts-ignore
|
(state: RootState, notification: NotificationGroup) => selectAccounts(state, notification.sample_account_ids),
|
||||||
(state: RootState, notification: MinifiedNotification) => state.statuses.get(notification.status_id),
|
], (notification, target, status, accounts): SelectedNotification => ({
|
||||||
(state: RootState, notification: MinifiedNotification) => notification.account_ids ? selectAccounts(state, notification.account_ids) : null,
|
|
||||||
], (notification, account, target, status, accounts): MinifiedNotification & Notification => ({
|
|
||||||
...notification,
|
...notification,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
account: account || null,
|
target,
|
||||||
// @ts-ignore
|
|
||||||
target: target || null,
|
|
||||||
// @ts-ignore
|
|
||||||
status: status || null,
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
status,
|
||||||
accounts,
|
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 & {
|
type AccountGalleryAttachment = MediaAttachment & {
|
||||||
status: MinifiedStatus;
|
status: MinifiedStatus;
|
||||||
account: BaseAccount;
|
account: BaseAccount;
|
||||||
|
@ -357,6 +364,7 @@ export {
|
||||||
makeGetStatus,
|
makeGetStatus,
|
||||||
type SelectedStatus,
|
type SelectedStatus,
|
||||||
makeGetNotification,
|
makeGetNotification,
|
||||||
|
type SelectedNotification,
|
||||||
type AccountGalleryAttachment,
|
type AccountGalleryAttachment,
|
||||||
getAccountGallery,
|
getAccountGallery,
|
||||||
getGroupGallery,
|
getGroupGallery,
|
||||||
|
|
|
@ -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"
|
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.once@^4.0.0:
|
lodash.once@^4.0.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
|
@ -7570,13 +7575,14 @@ pkg-dir@^4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
pl-api@^0.1.5:
|
pl-api@^0.1.7:
|
||||||
version "0.1.5"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.5.tgz#373d58fc40ae23b12c6d20def8d3332112e2dd93"
|
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.1.7.tgz#929b557903b280bda9e7c7bf6aa8cd015698c42f"
|
||||||
integrity sha512-IMcwANPtTRMv+tTLW4ic/o/usYIHeapR+aOx5YhmmVQsXTiVUZQtCVLmrfIrqMnyXm8PHvE/arzzozRFU8+GXQ==
|
integrity sha512-cTUHacufHDlNl7zHsOH+LW5MvotY5pUnBhfFMofVfvGOswURl0F1x1L8+V5k1D4VGn+mE7CnOv3esz/3QrrQmg==
|
||||||
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"
|
||||||
|
|
Loading…
Reference in a new issue