From 882f6814d22e3cb0ae9fb823dabba98e85ef56f2 Mon Sep 17 00:00:00 2001 From: Yanay Date: Mon, 6 Jul 2026 00:07:36 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(push):=20node=20SDK=20=E2=80=94=20noti?= =?UTF-8?q?fication-preferences=20+=20conversation-mute=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the server routes: push.getNotificationPreferences / updateNotificationPreferences and chat.muteConversation, with server-exact names and PUSH_EVENT_TYPES/MUTE_DURATIONS. These routes are acting-user-scoped (no service-key impersonation), so no userId param. Co-Authored-By: Claude Opus 4.8 --- src/index.ts | 11 +++++ src/interfaces/ConversationMember.ts | 8 +++- src/interfaces/Push.ts | 46 +++++++++++++++++++ src/modules/chat/index.ts | 1 + src/modules/chat/muteConversation.ts | 37 +++++++++++++++ .../push/getNotificationPreferences.ts | 20 ++++++++ src/modules/push/index.ts | 2 + .../push/updateNotificationPreferences.ts | 31 +++++++++++++ 8 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/modules/chat/muteConversation.ts create mode 100644 src/modules/push/getNotificationPreferences.ts create mode 100644 src/modules/push/updateNotificationPreferences.ts diff --git a/src/index.ts b/src/index.ts index ea156a7..bcc2efd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,6 +155,17 @@ export type { Reaction, ReactionType, ReactionCounts } from "./interfaces/Reacti export type { UnifiedAppNotification, PotentiallyPopulatedUnifiedAppNotification } from "./interfaces/AppNotification"; export type { OAuthIdentity } from "./interfaces/OAuthIdentity"; export type { PushDeviceResult, SendPushResult } from "./interfaces/Push"; +export { PUSH_EVENT_TYPES, MUTE_DURATIONS } from "./interfaces/Push"; +export type { + PushEventType, + MuteDuration, + NotificationPreferences, +} from "./interfaces/Push"; +export type { UpdateNotificationPreferencesProps } from "./modules/push/updateNotificationPreferences"; +export type { + MuteConversationProps, + MuteConversationResult, +} from "./modules/chat/muteConversation"; export type { Report, CreateReportResponse } from "./interfaces/Report"; export type { File, FileImage, FileImageVariant } from "./interfaces/File"; export type { diff --git a/src/interfaces/ConversationMember.ts b/src/interfaces/ConversationMember.ts index a3e7ae9..72c37d1 100644 --- a/src/interfaces/ConversationMember.ts +++ b/src/interfaces/ConversationMember.ts @@ -9,7 +9,13 @@ export interface ConversationMember { userId: string; role: ConversationMemberRole | null; lastReadAt: string | null; - mutedUntil: string | null; + // Personal mute state, present ONLY on the viewer's own row (omitted for + // other members). Timed mute → real ISO timestamp; muted "forever" → + // `mutedUntil: null` with `mutedForever: true` (the storage sentinel is never + // exposed as a magic date). `null` + `mutedForever: false` = not muted. + mutedUntil?: string | null; + // Explicit "forever" signal for the viewer's own row. Absent on others' rows. + mutedForever?: boolean; isActive: boolean; leftAt: string | null; createdAt: string; diff --git a/src/interfaces/Push.ts b/src/interfaces/Push.ts index e092a1c..863f137 100644 --- a/src/interfaces/Push.ts +++ b/src/interfaces/Push.ts @@ -7,3 +7,49 @@ export interface PushDeviceResult { export interface SendPushResult { results: Record; } + +/** + * Every push event type — the full app-notification type set plus the chat + * `message` event (push-only). Mirrors the server's `PUSH_EVENT_TYPES` exactly + * (server `src/constants/push/pushEvents.ts`); these are the only names a + * `disabledTypes` set may contain. + */ +export const PUSH_EVENT_TYPES = [ + "entity-comment", + "comment-reply", + "entity-mention", + "comment-mention", + "entity-upvote", + "comment-upvote", + "entity-reaction", + "comment-reaction", + "entity-reaction-milestone-specific", + "entity-reaction-milestone-total", + "comment-reaction-milestone-specific", + "comment-reaction-milestone-total", + "new-follow", + "connection-request", + "connection-accepted", + "space-membership-approved", + "event-invite", + "event-updated", + "event-cancelled", + "message", +] as const; + +export type PushEventType = (typeof PUSH_EVENT_TYPES)[number]; + +/** + * The four client-facing conversation-mute duration choices. Send the CHOICE, + * never a raw timestamp — the server resolves it and represents "forever" via + * the explicit `mutedForever` signal on the returned member. Mirrors the + * server's `MUTE_DURATIONS` (server `src/helpers/push/muteDuration.ts`). + */ +export const MUTE_DURATIONS = ["8h", "24h", "1w", "forever"] as const; + +export type MuteDuration = (typeof MUTE_DURATIONS)[number]; + +/** Response of read/update notification preferences. */ +export interface NotificationPreferences { + disabledTypes: PushEventType[]; +} diff --git a/src/modules/chat/index.ts b/src/modules/chat/index.ts index 56e162c..6cda223 100644 --- a/src/modules/chat/index.ts +++ b/src/modules/chat/index.ts @@ -19,3 +19,4 @@ export { toggleReaction } from "./toggleReaction"; export { listReactions } from "./listReactions"; export { markAsRead } from "./markAsRead"; export { reportMessage } from "./reportMessage"; +export { muteConversation } from "./muteConversation"; diff --git a/src/modules/chat/muteConversation.ts b/src/modules/chat/muteConversation.ts new file mode 100644 index 0000000..59f6656 --- /dev/null +++ b/src/modules/chat/muteConversation.ts @@ -0,0 +1,37 @@ +import { SublayHttpClient } from "../../core/client"; +import { ConversationMember } from "../../interfaces/ConversationMember"; +import { MuteDuration } from "../../interfaces/Push"; + +export interface MuteConversationProps { + conversationId: string; + /** + * The duration CHOICE (`8h` / `24h` / `1w` / `forever`), or `null` to clear + * the mute. Never a raw timestamp — the server resolves it and represents + * "forever" via the returned member's explicit `mutedForever` signal. + */ + duration: MuteDuration | null; +} + +export interface MuteConversationResult { + currentMember: ConversationMember; +} + +/** + * Set / clear the acting user's conversation mute. + * + * Mirrors `POST /:projectId/chat/conversations/:conversationId/mute`. This route + * is acting-user-scoped (a user only mutes their own membership — no + * service-key impersonation path), so there is no `userId` parameter. + */ +export async function muteConversation( + client: SublayHttpClient, + data: MuteConversationProps +): Promise { + const { conversationId, duration } = data; + const response = + await client.projectInstance.post( + `/chat/conversations/${conversationId}/mute`, + { duration } + ); + return response.data; +} diff --git a/src/modules/push/getNotificationPreferences.ts b/src/modules/push/getNotificationPreferences.ts new file mode 100644 index 0000000..2de1dc1 --- /dev/null +++ b/src/modules/push/getNotificationPreferences.ts @@ -0,0 +1,20 @@ +import { SublayHttpClient } from "../../core/client"; +import { NotificationPreferences } from "../../interfaces/Push"; + +/** + * Read the acting user's push notification preferences (the set of event types + * disabled for push). Returns an empty `disabledTypes` (all-on) when no + * preference row exists. + * + * Mirrors `GET /:projectId/push-notifications/preferences`. This route is + * acting-user-scoped (the server reads the authenticated user directly and has + * no service-key impersonation path), so there is no `userId` parameter. + */ +export async function getNotificationPreferences( + client: SublayHttpClient +): Promise { + const response = await client.projectInstance.get( + `/push-notifications/preferences` + ); + return response.data; +} diff --git a/src/modules/push/index.ts b/src/modules/push/index.ts index 10be63c..5aa240b 100644 --- a/src/modules/push/index.ts +++ b/src/modules/push/index.ts @@ -1 +1,3 @@ export { send } from "./send"; +export { getNotificationPreferences } from "./getNotificationPreferences"; +export { updateNotificationPreferences } from "./updateNotificationPreferences"; diff --git a/src/modules/push/updateNotificationPreferences.ts b/src/modules/push/updateNotificationPreferences.ts new file mode 100644 index 0000000..6fd3950 --- /dev/null +++ b/src/modules/push/updateNotificationPreferences.ts @@ -0,0 +1,31 @@ +import { SublayHttpClient } from "../../core/client"; +import { + NotificationPreferences, + PushEventType, +} from "../../interfaces/Push"; + +export interface UpdateNotificationPreferencesProps { + /** + * The set of push event types the user has opted OUT of. Server-exact type + * names only (unknown names are rejected server-side); duplicates collapse. + */ + disabledTypes: PushEventType[]; +} + +/** + * Upsert the acting user's push notification preferences. + * + * Mirrors `PUT /:projectId/push-notifications/preferences`. Acting-user-scoped + * (no service-key impersonation path), so there is no `userId` parameter. + */ +export async function updateNotificationPreferences( + client: SublayHttpClient, + data: UpdateNotificationPreferencesProps +): Promise { + const { disabledTypes } = data; + const response = await client.projectInstance.put( + `/push-notifications/preferences`, + { disabledTypes } + ); + return response.data; +} From b3056ea25e3a675c4a904ec1deae8a88f8b90323 Mon Sep 17 00:00:00 2001 From: Yanay Date: Mon, 6 Jul 2026 00:38:34 +0300 Subject: [PATCH 2/2] =?UTF-8?q?feat(push):=20node=20SDK=20=E2=80=94=20user?= =?UTF-8?q?Id=20on=20mute=20+=20preference=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit muteConversation / getNotificationPreferences / updateNotificationPreferences now take a userId (GET via params, POST/PUT via body), matching node-sdk's existing per-user override convention, so the service-key client can act on behalf of a named user (these routes now accept the resolveActingUserId path). Co-Authored-By: Claude Opus 4.8 --- src/modules/chat/muteConversation.ts | 17 +++++++----- .../push/getNotificationPreferences.ts | 26 +++++++++++++------ .../push/updateNotificationPreferences.ts | 16 ++++++++---- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/modules/chat/muteConversation.ts b/src/modules/chat/muteConversation.ts index 59f6656..210398c 100644 --- a/src/modules/chat/muteConversation.ts +++ b/src/modules/chat/muteConversation.ts @@ -10,6 +10,11 @@ export interface MuteConversationProps { * "forever" via the returned member's explicit `mutedForever` signal. */ duration: MuteDuration | null; + /** + * The acting user whose membership is muted. The service key acts on this + * user's behalf; the returned `currentMember` reflects that user's row. + */ + userId: string; } export interface MuteConversationResult { @@ -17,21 +22,21 @@ export interface MuteConversationResult { } /** - * Set / clear the acting user's conversation mute. + * Set / clear a user's conversation mute. * - * Mirrors `POST /:projectId/chat/conversations/:conversationId/mute`. This route - * is acting-user-scoped (a user only mutes their own membership — no - * service-key impersonation path), so there is no `userId` parameter. + * Mirrors `POST /:projectId/chat/conversations/:conversationId/mute`. The route + * is acting-user-scoped; the SDK authenticates with a service key and names the + * acting user via `userId` (like the other per-user operations). */ export async function muteConversation( client: SublayHttpClient, data: MuteConversationProps ): Promise { - const { conversationId, duration } = data; + const { conversationId, duration, userId } = data; const response = await client.projectInstance.post( `/chat/conversations/${conversationId}/mute`, - { duration } + { duration, userId } ); return response.data; } diff --git a/src/modules/push/getNotificationPreferences.ts b/src/modules/push/getNotificationPreferences.ts index 2de1dc1..2c65858 100644 --- a/src/modules/push/getNotificationPreferences.ts +++ b/src/modules/push/getNotificationPreferences.ts @@ -1,20 +1,30 @@ import { SublayHttpClient } from "../../core/client"; import { NotificationPreferences } from "../../interfaces/Push"; +export interface GetNotificationPreferencesProps { + /** + * The acting user whose preferences are read. The service key acts on this + * user's behalf. + */ + userId: string; +} + /** - * Read the acting user's push notification preferences (the set of event types - * disabled for push). Returns an empty `disabledTypes` (all-on) when no - * preference row exists. + * Read a user's push notification preferences (the set of event types disabled + * for push). Returns an empty `disabledTypes` (all-on) when no preference row + * exists. * - * Mirrors `GET /:projectId/push-notifications/preferences`. This route is - * acting-user-scoped (the server reads the authenticated user directly and has - * no service-key impersonation path), so there is no `userId` parameter. + * Mirrors `GET /:projectId/push-notifications/preferences`. The route is + * acting-user-scoped; the SDK authenticates with a service key and names the + * acting user via the `userId` query param (like the other per-user reads). */ export async function getNotificationPreferences( - client: SublayHttpClient + client: SublayHttpClient, + data: GetNotificationPreferencesProps ): Promise { const response = await client.projectInstance.get( - `/push-notifications/preferences` + `/push-notifications/preferences`, + { params: { userId: data.userId } } ); return response.data; } diff --git a/src/modules/push/updateNotificationPreferences.ts b/src/modules/push/updateNotificationPreferences.ts index 6fd3950..84dd018 100644 --- a/src/modules/push/updateNotificationPreferences.ts +++ b/src/modules/push/updateNotificationPreferences.ts @@ -10,22 +10,28 @@ export interface UpdateNotificationPreferencesProps { * names only (unknown names are rejected server-side); duplicates collapse. */ disabledTypes: PushEventType[]; + /** + * The acting user whose preferences are written. The service key acts on this + * user's behalf. + */ + userId: string; } /** - * Upsert the acting user's push notification preferences. + * Upsert a user's push notification preferences. * - * Mirrors `PUT /:projectId/push-notifications/preferences`. Acting-user-scoped - * (no service-key impersonation path), so there is no `userId` parameter. + * Mirrors `PUT /:projectId/push-notifications/preferences`. The route is + * acting-user-scoped; the SDK authenticates with a service key and names the + * acting user via `userId` (like the other per-user operations). */ export async function updateNotificationPreferences( client: SublayHttpClient, data: UpdateNotificationPreferencesProps ): Promise { - const { disabledTypes } = data; + const { disabledTypes, userId } = data; const response = await client.projectInstance.put( `/push-notifications/preferences`, - { disabledTypes } + { disabledTypes, userId } ); return response.data; }