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..210398c --- /dev/null +++ b/src/modules/chat/muteConversation.ts @@ -0,0 +1,42 @@ +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; + /** + * 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 { + currentMember: ConversationMember; +} + +/** + * Set / clear a user's conversation mute. + * + * 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, userId } = data; + const response = + await client.projectInstance.post( + `/chat/conversations/${conversationId}/mute`, + { duration, userId } + ); + return response.data; +} diff --git a/src/modules/push/getNotificationPreferences.ts b/src/modules/push/getNotificationPreferences.ts new file mode 100644 index 0000000..2c65858 --- /dev/null +++ b/src/modules/push/getNotificationPreferences.ts @@ -0,0 +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 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`. 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, + data: GetNotificationPreferencesProps +): Promise { + const response = await client.projectInstance.get( + `/push-notifications/preferences`, + { params: { userId: data.userId } } + ); + 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..84dd018 --- /dev/null +++ b/src/modules/push/updateNotificationPreferences.ts @@ -0,0 +1,37 @@ +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[]; + /** + * The acting user whose preferences are written. The service key acts on this + * user's behalf. + */ + userId: string; +} + +/** + * Upsert a user's push notification preferences. + * + * 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, userId } = data; + const response = await client.projectInstance.put( + `/push-notifications/preferences`, + { disabledTypes, userId } + ); + return response.data; +}