diff --git a/docs.json b/docs.json index f17ba2a8b..a2c6228d8 100644 --- a/docs.json +++ b/docs.json @@ -1169,6 +1169,8 @@ "ui-kit/react-native/group-members", "ui-kit/react-native/message-header", "ui-kit/react-native/message-list", + "ui-kit/react-native/pinned-messages", + "ui-kit/react-native/saved-messages", "ui-kit/react-native/message-composer", "ui-kit/react-native/compact-message-composer", "ui-kit/react-native/threaded-messages-header", @@ -3540,10 +3542,14 @@ "sdk/react-native/additional-message-filtering", "sdk/react-native/retrieve-conversations", "sdk/react-native/threaded-messages", + "sdk/react-native/thread-subscription", "sdk/react-native/edit-message", "sdk/react-native/delete-message", "sdk/react-native/flag-message", + "sdk/react-native/pin-message", + "sdk/react-native/save-message", "sdk/react-native/delete-conversation", + "sdk/react-native/pin-conversation", "sdk/react-native/typing-indicators", "sdk/react-native/transient-messages", "sdk/react-native/delivery-read-receipts", diff --git a/sdk/react-native/additional-message-filtering.mdx b/sdk/react-native/additional-message-filtering.mdx index 74ce76e6e..19a60f89d 100644 --- a/sdk/react-native/additional-message-filtering.mdx +++ b/sdk/react-native/additional-message-filtering.mdx @@ -1473,6 +1473,101 @@ let messagesRequest = new CometChat.MessagesRequestBuilder() The response contains media message objects filtered to the specified attachment types. +## Pinned messages + +Use `setPinnedOnly(true)` to fetch only the pinned messages of a conversation. A pinned list is conversation-scoped, so pair it with `setUID()` or `setGUID()` — exactly one of the two is required. + + + +```typescript +let UID: string = "UID", + limit: number = 50, + messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setUID(UID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + +```javascript +let UID = "UID"; +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setUID(UID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + +```typescript +let GUID: string = "GUID", + limit: number = 50, + messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setGUID(GUID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + +```javascript +let GUID = "GUID"; +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setGUID(GUID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + + +The list is ordered by pin time, most recently pinned first — not by when the messages were sent. See [Pin A Message](/sdk/react-native/pin-message) for the full feature. + +## Saved messages + +Use `setSavedOnly(true)` to fetch the logged-in user's saved messages. The saved list is user-level and spans every conversation, so unlike the pinned list you do **not** set a UID or a GUID. + + + +```typescript +let limit: number = 50, + messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setLimit(limit) + .setSavedOnly(true) + .build(); +``` + + + + +```javascript +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setLimit(limit) + .setSavedOnly(true) + .build(); +``` + + + + + +Saves are private to the logged-in user. Because the rows come from different conversations, use `getConversationId()`, `getReceiverType()` and `getReceiverId()` on each message to route back to the right conversation. See [Save A Message](/sdk/react-native/save-message) for the full feature. + ## Next Steps diff --git a/sdk/react-native/pin-conversation.mdx b/sdk/react-native/pin-conversation.mdx new file mode 100644 index 000000000..a27cffc80 --- /dev/null +++ b/sdk/react-native/pin-conversation.mdx @@ -0,0 +1,396 @@ +--- +title: "Pin A Conversation" +sidebarTitle: "Pin A Conversation" +description: "Pin and unpin conversations and fetch the pinned conversation list with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Pin / unpin — addressed by peer + type, not by conversationId +await CometChat.pinConversation("UID", CometChat.RECEIVER_TYPE.USER); +await CometChat.unpinConversation("GUID", CometChat.RECEIVER_TYPE.GROUP); + +// The DEFAULT list is already pin-ordered — no filter needed. +// To narrow to pins only, pass the raw string tokens: +const request = new CometChat.ConversationsRequestBuilder() + .setPinnedBy(["system", "me"]) + .setLimit(30) + .build(); + +// Read pin state — there is no isPinned() +const isPinned = conversation.getPinnedAt() !== undefined; +const isSystemPinned = conversation.getPinnedBy() === "app_system"; + +// Cap and availability (off by default — must be mapped per app) +const limit = await CometChat.getPinConversationLimit(); +const enabled = await CometChat.isPinConversationEnabled(); +``` + + + +Pinning a conversation keeps it at the top of the logged-in user's conversation list. The pin is **private to that user** — nobody else sees it — and it syncs to their other devices. + + + Pin Conversation requires the `features.ux.conversations.pinned.enabled` flag, + which is **not seeded in any plan** and must be mapped per app. Until it is, + every call rejects with `ERR_FEATURE_NOT_ACCESSIBLE`. Gate your UI on + `isPinConversationEnabled()` before showing the control. + + + + This is separate from an **admin-global pin**, which is managed from the + [CometChat Dashboard](https://app.cometchat.com) and shows for every user. + Those cannot be created or removed from the SDK, only observed. + + +## Pin a Conversation + +A conversation is addressed by its peer — the other user's UID for a one-on-one conversation, or the GUID for a group — together with the conversation type. + + + +```typescript +CometChat.pinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +).then( + (conversation: CometChat.Conversation) => { + console.log("Conversation pinned:", conversation); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to pin conversation:", error); + } +); +``` + + + + +```javascript +CometChat.pinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +).then( + (conversation) => { + console.log("Conversation pinned:", conversation); + }, + (error) => { + console.log("Failed to pin conversation:", error); + } +); +``` + + + + +```typescript +CometChat.pinConversation( + "cometchat-guid-1", + CometChat.RECEIVER_TYPE.GROUP +).then( + (conversation: CometChat.Conversation) => { + console.log("Conversation pinned:", conversation); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to pin conversation:", error); + } +); +``` + + + + +```javascript +CometChat.pinConversation( + "cometchat-guid-1", + CometChat.RECEIVER_TYPE.GROUP +).then( + (conversation) => { + console.log("Conversation pinned:", conversation); + }, + (error) => { + console.log("Failed to pin conversation:", error); + } +); +``` + + + + + +It resolves with the full updated `Conversation`, with `pinnedAt` and `pinnedBy` set. Pinning is idempotent. + + + A conversation that has **never been messaged**, or that is hidden, cannot be + newly pinned — the call rejects with `ERR_CONVERSATION_NOT_ACCESSIBLE`. Pin + from a conversation that already exists in the list. + + +## Unpin a Conversation + + + +```typescript +CometChat.unpinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +).then( + (conversation: CometChat.Conversation) => { + console.log("Conversation unpinned:", conversation); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to unpin conversation:", error); + } +); +``` + + + + +```javascript +CometChat.unpinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +).then( + (conversation) => { + console.log("Conversation unpinned:", conversation); + }, + (error) => { + console.log("Failed to unpin conversation:", error); + } +); +``` + + + + + +Unpinning returns the conversation to its recency position. A user **cannot** unpin an admin-global pin — hide or disable the unpin control for conversations where `getPinnedBy()` is `"app_system"`. + +## Fetch Pinned Conversations + +The default conversation list is already **pin-ordered** by the server: admin-global pins first, then the user's own pins, then everything else. Each row already carries `pinnedAt` and `pinnedBy`, so a pinned strip usually needs **no separate call**. + +When you do want the pinned set standalone, use `setPinnedBy()`. + + + +```typescript +let conversationsRequest: CometChat.ConversationsRequest = + new CometChat.ConversationsRequestBuilder() + .setPinnedBy(["system", "me"]) + .setLimit(30) + .build(); + +conversationsRequest.fetchNext().then( + (conversations: CometChat.Conversation[]) => { + console.log("Pinned conversations:", conversations); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to fetch conversations:", error); + } +); +``` + + + + +```javascript +let conversationsRequest = new CometChat.ConversationsRequestBuilder() + .setPinnedBy(["system", "me"]) + .setLimit(30) + .build(); + +conversationsRequest.fetchNext().then( + (conversations) => { + console.log("Pinned conversations:", conversations); + }, + (error) => { + console.log("Failed to fetch conversations:", error); + } +); +``` + + + + + +| Token | Meaning | +| ------------ | ---------------------------------------------------- | +| `"me"` | Conversations the logged-in user pinned themselves. | +| `"system"` | Conversations pinned globally by an admin. | + + + The match is **exact and case-sensitive**, and an unrecognised value is dropped + silently rather than raising. `setPinnedBy(["Me"])` leaves nothing behind, the + `pinnedBy` parameter is omitted entirely, and you get the **full conversation + list** back — not an error, and not a pinned-only list. Passing an empty array + does the same thing. Use the lowercase tokens exactly as written above. + + + + These are plain strings in the React Native SDK — there is no + `CometChat.PINNED_BY` constant to import. The server rejects any other value + with `ERR_BAD_REQUEST` — end users cannot query another user's pins — so the + builder drops unrecognised entries before the request is sent. + + +## Check if a Conversation is Pinned + +As with messages, the React Native SDK has **no `isPinned()` helper** — the presence of `pinnedAt` *is* the boolean. + + + +```typescript +const isPinned: boolean = conversation.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", conversation.getPinnedAt()); + console.log("Pinned by:", conversation.getPinnedBy()); + + // A user may not unpin an admin/global pin + const isSystemPinned: boolean = conversation.getPinnedBy() === "app_system"; +} +``` + + + + +```javascript +const isPinned = conversation.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", conversation.getPinnedAt()); + console.log("Pinned by:", conversation.getPinnedBy()); + + // A user may not unpin an admin/global pin + const isSystemPinned = conversation.getPinnedBy() === "app_system"; +} +``` + + + + + +| Method | Returns | +| ----------------- | ------------------------------------------------------------------------------- | +| `getPinnedAt()` | The pin timestamp, or `undefined` when the conversation is not pinned. | +| `getPinnedBy()` | The pinner's UID, or `"app_system"` for an admin/global pin, or `undefined`. | + +## Keeping the List in Sync + + + The React Native SDK does **not** expose a `ConversationListener`, and there + are no `onConversationPinned` / `onConversationUnpinned` callbacks. Conversation + pins made on another device do not arrive over the socket. + + +Update your list from the promise on the device that performed the action, and re-fetch the conversation list when the screen regains focus to pick up pins made elsewhere. + + + +```typescript +const conversation: CometChat.Conversation = await CometChat.pinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +); + +// The server accepted it — move the row yourself. +setConversations((prev) => reorderWithPinnedFirst(prev, conversation)); +``` + + + + +```javascript +CometChat.pinConversation("cometchat-uid-1", CometChat.RECEIVER_TYPE.USER).then( + (conversation) => { + // The server accepted it — move the row yourself. + setConversations((prev) => reorderWithPinnedFirst(prev, conversation)); + } +); +``` + + + + + +## Pin Limit + + + +```typescript +let limit: number | null = await CometChat.getPinConversationLimit(); +let systemLimit: number | null = await CometChat.getSystemPinConversationLimit(); +``` + + + + +```javascript +CometChat.getPinConversationLimit().then((limit) => { + console.log("Pinned conversations limit:", limit); +}); +``` + + + + + +Both resolve to `null` when the app settings carry no value. `getSystemPinConversationLimit()` is the separate admin/global cap, enforced independently — system pins do not consume a user's allowance. + + + The conversation pin cap and the message pin cap are **separate quotas with + different values**, and both are server-owned and tenant-overridable. Read + each one at runtime rather than assuming a number or reusing the other. + + +## Feature Availability + + + +```typescript +let enabled: boolean = await CometChat.isPinConversationEnabled(); + +if (enabled) { + // Show the Pin Conversation option +} +``` + + + + +```javascript +CometChat.isPinConversationEnabled().then((enabled) => { + if (enabled) { + // Show the Pin Conversation option + } +}); +``` + + + + + +Unlike the two message-level flags, this one is **not seeded anywhere and is off by default** — treat a `false` as the normal case for a new app, not as an error. + +--- + +## Next Steps + + + + Fetch and order the conversation list + + + Highlight a message for everyone in a conversation + + + Bookmark a message privately, across conversations + + + Remove a conversation from the list + + diff --git a/sdk/react-native/pin-message.mdx b/sdk/react-native/pin-message.mdx new file mode 100644 index 000000000..a5b713509 --- /dev/null +++ b/sdk/react-native/pin-message.mdx @@ -0,0 +1,426 @@ +--- +title: "Pin A Message" +sidebarTitle: "Pin A Message" +description: "Pin and unpin messages in a conversation, fetch the pinned list, and listen for pin events with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Pin / unpin a message +const pinned = await CometChat.pinMessage(messageId); +const unpinned = await CometChat.unpinMessage(messageId); + +// Fetch the pinned list for one conversation +const request = new CometChat.MessagesRequestBuilder() + .setUID("UID") // or .setGUID("GUID") + .setPinnedOnly(true) + .setLimit(50) + .build(); +const messages = await request.fetchPrevious(); + +// Read pin state off a message — there is no isPinned() +const isPinned = message.getPinnedAt() !== undefined; + +// Caps and availability +const limit = await CometChat.getPinMessageLimit(); // number | null +const enabled = await CometChat.isPinMessageEnabled(); // boolean +``` + + + +Pinning highlights an important message in a conversation. A pin is **conversation-wide and visible to everyone** in that conversation, so it is the right tool for announcements, rules or a link everyone keeps asking for. + + + Pinning is a moderation action. Only an Admin, Moderator or group Owner may pin + or unpin. The server is the authority: a member's call is rejected with + `ERR_ACTION_NOT_ALLOWED`. + + +## Pin a Message + +Call `pinMessage()` with the message's ID. It resolves with the full updated message, with `pinnedAt` and `pinnedBy` set. + + + +```typescript +let messageId: number = 100; + +CometChat.pinMessage(messageId).then( + (message: CometChat.BaseMessage) => { + console.log("Message pinned:", message); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to pin message:", error); + } +); +``` + + + + +```javascript +let messageId = 100; + +CometChat.pinMessage(messageId).then( + (message) => { + console.log("Message pinned:", message); + }, + (error) => { + console.log("Failed to pin message:", error); + } +); +``` + + + + + +Pinning is **idempotent**, and a message has a single pinner: re-pinning an already pinned message updates `pinnedBy` and `pinnedAt` to the most recent pinner rather than failing. + + + **A just-sent message may not be pinnable or savable yet.** On an app with + moderation enabled, the server stamps a new message `moderation.status: + "pending"` and clears it to `"approved"` a moment later. While it is pending, + `pinMessage()` rejects with `403 ERR_MESSAGE_NO_ACCESS` — the **same code it returns + for a genuine permission refusal**, so you cannot tell the two apart from the + error alone. + + Measured on a moderated app, the window runs from **send**, not from the user's + tap, and clears within a few seconds. Do not disable the control on this error: + by the time someone opens a menu and taps, moderation has usually finished. + Prefer a retry or a transient "not ready yet" message over telling the user + they lack permission. Moderation is configured **per app**, so this never + reproduces on an app that has it switched off. + + + + On a cap breach the rejection carries the server-owned ceiling in + `errorParams.limit`, so you can tell the user the actual number without a + second call. + + +## Unpin a Message + + + +```typescript +CometChat.unpinMessage(100).then( + (message: CometChat.BaseMessage) => { + console.log("Message unpinned:", message); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to unpin message:", error); + } +); +``` + + + + +```javascript +CometChat.unpinMessage(100).then( + (message) => { + console.log("Message unpinned:", message); + }, + (error) => { + console.log("Failed to unpin message:", error); + } +); +``` + + + + + +Any Admin, Moderator or Owner may unpin — not only whoever pinned it. The resolved message comes back with its pin attributes cleared, so you can swap the rendered message straight into your list. + +## Fetch Pinned Messages + +Build a `MessagesRequest` with `setPinnedOnly(true)`. A pinned list belongs to one conversation, so pair it with `setUID()` for a one-on-one conversation or `setGUID()` for a group — exactly one of the two is required. + + + +```typescript +let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setUID("cometchat-uid-1") + .setPinnedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages: CometChat.BaseMessage[]) => { + console.log("Pinned messages:", messages); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to fetch pinned messages:", error); + } +); +``` + + + + +```javascript +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setUID("cometchat-uid-1") + .setPinnedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages) => { + console.log("Pinned messages:", messages); + }, + (error) => { + console.log("Failed to fetch pinned messages:", error); + } +); +``` + + + + +```typescript +let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setGUID("cometchat-guid-1") + .setPinnedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages: CometChat.BaseMessage[]) => { + console.log("Pinned messages:", messages); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to fetch pinned messages:", error); + } +); +``` + + + + +```javascript +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setGUID("cometchat-guid-1") + .setPinnedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages) => { + console.log("Pinned messages:", messages); + }, + (error) => { + console.log("Failed to fetch pinned messages:", error); + } +); +``` + + + + + +The list is ordered by **pin time, most recently pinned first** — not by when the messages were sent. Render it in the order the SDK returns it. + +The default value for `setLimit` is 30 and the max value is 100. + +Page it like any other message list: `fetchPrevious()` for older rows, `fetchNext()` for newer, stopping when a call resolves `[]`. The one difference is invisible to you — the cursor rides on `pinnedAt` instead of `sentAt`, and the SDK swaps it for you. + +## Check if a Message is Pinned + +Every `BaseMessage` carries its pin state as attributes. The React Native SDK has **no `isPinned()` helper** — the presence of `pinnedAt` *is* the boolean, and an unpinned message simply has no pin attributes. + + + +```typescript +const isPinned: boolean = message.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", message.getPinnedAt()); + console.log("Pinned by:", message.getPinnedBy()); + + // An admin/global pin is stamped with the reserved "app_system" pinner + // rather than a real UID — render it as a system pin, not as a user. + const isSystemPinned: boolean = message.getPinnedBy() === "app_system"; +} +``` + + + + +```javascript +const isPinned = message.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", message.getPinnedAt()); + console.log("Pinned by:", message.getPinnedBy()); + + // An admin/global pin is stamped with the reserved "app_system" pinner + // rather than a real UID — render it as a system pin, not as a user. + const isSystemPinned = message.getPinnedBy() === "app_system"; +} +``` + + + + + +| Method | Returns | +| ----------------- | ------------------------------------------------------------------- | +| `getPinnedAt()` | The pin timestamp, or `undefined` when the message is not pinned. | +| `getPinnedBy()` | The UID of the most recent pinner, or `undefined`. | + + + Do not port `isPinned()` or `isSystemPinned()` from the JavaScript SDK — they + do not exist in the React Native SDK. Derive both from `getPinnedAt()` and + `getPinnedBy()` as shown above. + + +## Real-time Pin Events + +Pin and unpin are broadcast to everyone in the conversation. Add the callbacks to your existing `MessageListener`. + + + +```typescript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessagePinned: (message: CometChat.BaseMessage) => { + console.log("Message pinned:", message); + }, + onMessageUnpinned: (message: CometChat.BaseMessage) => { + console.log("Message unpinned:", message); + }, + }) +); +``` + + + + +```javascript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessagePinned: (message) => { + console.log("Message pinned:", message); + }, + onMessageUnpinned: (message) => { + console.log("Message unpinned:", message); + }, + }) +); +``` + + + + + +Each callback receives the **full updated message**, so `getPinnedAt()` and `getPinnedBy()` are readable without a follow-up fetch — replace the message in your list directly. + +Remove the listener when the screen unmounts: + + + +```typescript +CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); +``` + + + + +```javascript +CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); +``` + + + + + +## Pin Limit + +A conversation holds a capped number of pins, configurable per app. Read the cap rather than hard-coding it — it is tenant-overridable and will drift. + + + +```typescript +let limit: number | null = await CometChat.getPinMessageLimit(); +let systemLimit: number | null = await CometChat.getSystemPinMessageLimit(); + +if (limit !== null && pinnedCount >= limit) { + // Disable the pin control instead of letting the user hit the error +} +``` + + + + +```javascript +CometChat.getPinMessageLimit().then((limit) => { + if (limit !== null && pinnedCount >= limit) { + // Disable the pin control instead of letting the user hit the error + } +}); +``` + + + + + +Both resolve to `null` when the app settings carry no value — show generic copy in that case rather than guessing a number. `getSystemPinMessageLimit()` is the separate cap for admin/global pins: system pins do **not** consume a user's allowance, so the two budgets are enforced independently. + +## Feature Availability + +Check whether Pin Message is enabled for your app before showing pin actions. + + + +```typescript +let enabled: boolean = await CometChat.isPinMessageEnabled(); + +if (enabled) { + // Show the Pin option +} +``` + + + + +```javascript +CometChat.isPinMessageEnabled().then((enabled) => { + if (enabled) { + // Show the Pin option + } +}); +``` + + + + + +This resolves `false` rather than rejecting when the flag is missing or settings are unavailable, so an unavailable setting degrades to "hidden" instead of an unhandled rejection. + +--- + +## Next Steps + + + + Bookmark a message privately, across conversations + + + Pin a conversation to the top of the list + + + Every listener the SDK exposes, in one place + + + Filter messages by pinned, saved, type, tags and more + + diff --git a/sdk/react-native/real-time-listeners.mdx b/sdk/react-native/real-time-listeners.mdx index 1a8a385c1..cdfba1ec4 100644 --- a/sdk/react-native/real-time-listeners.mdx +++ b/sdk/react-native/real-time-listeners.mdx @@ -352,6 +352,10 @@ Receive events for incoming messages, typing indicators, read/delivery receipts, | **onMessageReactionAdded(receipt: [CometChat.ReactionEvent](/sdk/reference/auxiliary#reactionevent))** | This event is triggered when a reaction is added to a message in a user/group conversation. | | **onMessageReactionRemoved(receipt: [CometChat.ReactionEvent](/sdk/reference/auxiliary#reactionevent))** | This event is triggered when a reaction is removed from a message in a user/group conversation. | | **onCardMessageReceived(message: CometChat.CardMessage)** | This event is triggered when a card message (`category: "card"`) is received. See [Campaigns → Card Messages](/sdk/react-native/campaigns#card-messages). | +| **onMessagePinned(message: CometChat.BaseMessage)** | This event is triggered when a message is pinned in a user/group conversation. Broadcast to everyone in that conversation. | +| **onMessageUnpinned(message: CometChat.BaseMessage)** | This event is triggered when a message is unpinned in a user/group conversation. | +| **onMessageSaved(message: CometChat.BaseMessage)** | This event is triggered when the logged-in user saves a message. Private and delivered to that user's **other** devices only. | +| **onMessageUnsaved(message: CometChat.BaseMessage)** | This event is triggered when the logged-in user unsaves a message. | To add the `MessageListener`: @@ -406,6 +410,18 @@ CometChat.addMessageListener( onCardMessageReceived: (cardMessage: CometChat.CardMessage) => { console.log("Card message received", cardMessage); }, + onMessagePinned: (message: CometChat.BaseMessage) => { + console.log("Message pinned", message); + }, + onMessageUnpinned: (message: CometChat.BaseMessage) => { + console.log("Message unpinned", message); + }, + onMessageSaved: (message: CometChat.BaseMessage) => { + console.log("Message saved", message); + }, + onMessageUnsaved: (message: CometChat.BaseMessage) => { + console.log("Message unsaved", message); + }, }) ); ``` @@ -462,6 +478,18 @@ CometChat.addMessageListener( onCardMessageReceived: (cardMessage) => { console.log("Card message received", cardMessage); }, + onMessagePinned: (message) => { + console.log("Message pinned", message); + }, + onMessageUnpinned: (message) => { + console.log("Message unpinned", message); + }, + onMessageSaved: (message) => { + console.log("Message saved", message); + }, + onMessageUnsaved: (message) => { + console.log("Message unsaved", message); + }, }) ); ``` @@ -491,6 +519,24 @@ CometChat.removeMessageListener(listenerID); + + Pin and unpin are **conversation-wide** — every participant receives them. + Save and unsave are **private multi-device**: they reach the saving user's + other sessions, not the device that performed the save, which already has the + message resolved from the `saveMessage()` promise. Each callback carries the + full updated message, so `getPinnedAt()` / `getSavedAt()` are readable without + a further fetch. See [Pin A Message](/sdk/react-native/pin-message) and + [Save A Message](/sdk/react-native/save-message). + + + + **Conversation pins have no listener.** The React Native SDK exposes no + `ConversationListener` and no `onConversationPinned` / + `onConversationUnpinned` callbacks — unlike the JavaScript SDK. Update your + list from the `pinConversation()` promise and re-fetch on focus. See + [Pin A Conversation](/sdk/react-native/pin-conversation). + + ## Call Listener Receive events for incoming and outgoing call state changes. diff --git a/sdk/react-native/save-message.mdx b/sdk/react-native/save-message.mdx new file mode 100644 index 000000000..558dfe91b --- /dev/null +++ b/sdk/react-native/save-message.mdx @@ -0,0 +1,382 @@ +--- +title: "Save A Message" +sidebarTitle: "Save A Message" +description: "Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Save / unsave a message +const saved = await CometChat.saveMessage(messageId); +const unsaved = await CometChat.unsaveMessage(messageId); + +// Fetch the saved list — user-level, so NO setUID()/setGUID() +const request = new CometChat.MessagesRequestBuilder() + .setSavedOnly(true) + .setLimit(50) + .build(); +const messages = await request.fetchPrevious(); + +// Read save state off a message — there is no isSaved() +const isSaved = message.getSavedAt() !== undefined; + +// Cap and availability +const limit = await CometChat.getSaveMessageLimit(); // number | null +const enabled = await CometChat.isSaveMessageEnabled(); // boolean +``` + + + +Saving bookmarks a message for the logged-in user. Unlike a pin, a save is **private and cross-conversation**: nobody else can see it, no role is required, and the saved list spans every conversation the user is part of. + + + `savedAt` is per-viewer. It is only ever populated in the acting user's own + context — you will never see another user's saves on a message, so the same + message reads saved for one user and unsaved for everyone else. + + +## Save a Message + +Call `saveMessage()` with the message's ID. It resolves with the full updated message, with `savedAt` set. + + + +```typescript +let messageId: number = 100; + +CometChat.saveMessage(messageId).then( + (message: CometChat.BaseMessage) => { + console.log("Message saved:", message); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to save message:", error); + } +); +``` + + + + +```javascript +let messageId = 100; + +CometChat.saveMessage(messageId).then( + (message) => { + console.log("Message saved:", message); + }, + (error) => { + console.log("Failed to save message:", error); + } +); +``` + + + + + +Saving is idempotent — saving an already saved message succeeds rather than failing. There is no role gate. On a cap breach the rejection carries the server-owned ceiling in `errorParams.limit`. + + + **A just-sent message may not be pinnable or savable yet.** On an app with + moderation enabled, the server stamps a new message `moderation.status: + "pending"` and clears it to `"approved"` a moment later. While it is pending, + `saveMessage()` rejects with `403 ERR_MESSAGE_NO_ACCESS` — the **same code it returns + for a genuine permission refusal**, so you cannot tell the two apart from the + error alone. + + Measured on a moderated app, the window runs from **send**, not from the user's + tap, and clears within a few seconds. Do not disable the control on this error: + by the time someone opens a menu and taps, moderation has usually finished. + Prefer a retry or a transient "not ready yet" message over telling the user + they lack permission. Moderation is configured **per app**, so this never + reproduces on an app that has it switched off. + + +## Unsave a Message + + + +```typescript +CometChat.unsaveMessage(100).then( + (message: CometChat.BaseMessage) => { + console.log("Message unsaved:", message); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to unsave message:", error); + } +); +``` + + + + +```javascript +CometChat.unsaveMessage(100).then( + (message) => { + console.log("Message unsaved:", message); + }, + (error) => { + console.log("Failed to unsave message:", error); + } +); +``` + + + + + +The resolved message comes back with `savedAt` cleared, never left stale. + +## Fetch Saved Messages + +Build a `MessagesRequest` with `setSavedOnly(true)`. The saved list is **user-level**, so unlike the pinned list you do **not** set a UID or a GUID — leaving both unset is what makes it cross-conversation. + + + +```typescript +let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setSavedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages: CometChat.BaseMessage[]) => { + console.log("Saved messages:", messages); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to fetch saved messages:", error); + } +); +``` + + + + +```javascript +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setSavedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages) => { + console.log("Saved messages:", messages); + }, + (error) => { + console.log("Failed to fetch saved messages:", error); + } +); +``` + + + + + +The default value for `setLimit` is 30 and the max value is 100. + +The list comes back newest save first. Call `fetchPrevious()` again on the same object to page through older entries — this filter changes what the list contains, not how it pages. + + + Do **not** combine `setSavedOnly(true)` with `setUID()` or `setGUID()`. Saves + are account-wide; adding a scope narrows the list to one conversation and + quietly hides the rest. + + +Because the list spans conversations, every row carries its own context — use `getConversationId()`, `getReceiverId()` and `getReceiverType()` to route a tap on a saved message back to the right conversation. + + + +```typescript +messages.forEach((message: CometChat.BaseMessage) => { + console.log( + message.getConversationId(), + message.getReceiverType(), // "user" or "group" + message.getReceiverId() + ); +}); +``` + + + + +```javascript +messages.forEach((message) => { + console.log( + message.getConversationId(), + message.getReceiverType(), // "user" or "group" + message.getReceiverId() + ); +}); +``` + + + + + +## Check if a Message is Saved + +The React Native SDK has **no `isSaved()` helper** — the presence of `savedAt` *is* the boolean, and an unsaved message simply has no save attribute. + + + +```typescript +const isSaved: boolean = message.getSavedAt() !== undefined; + +if (isSaved) { + console.log("Saved at:", message.getSavedAt()); +} +``` + + + + +```javascript +const isSaved = message.getSavedAt() !== undefined; + +if (isSaved) { + console.log("Saved at:", message.getSavedAt()); +} +``` + + + + + +| Method | Returns | +| --------------- | ------------------------------------------------------------------ | +| `getSavedAt()` | The save timestamp, or `undefined` when the user has not saved it. | + + + Do not port `isSaved()` from the JavaScript SDK — it does not exist in the + React Native SDK. Derive it from `getSavedAt()` as shown above. + + +## Real-time Save Events + +Save and unsave are **private multi-device** events: they are delivered to the user's other logged-in sessions so a save on the phone shows up on the desktop. Add the callbacks to your existing `MessageListener`. + + + +```typescript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessageSaved: (message: CometChat.BaseMessage) => { + console.log("Message saved:", message); + }, + onMessageUnsaved: (message: CometChat.BaseMessage) => { + console.log("Message unsaved:", message); + }, + }) +); +``` + + + + +```javascript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessageSaved: (message) => { + console.log("Message saved:", message); + }, + onMessageUnsaved: (message) => { + console.log("Message unsaved:", message); + }, + }) +); +``` + + + + + + + These callbacks fire for saves made on your **other** devices, not the one that + performed the save — that device already has the message resolved from + `saveMessage()`. Update your saved list from the promise there, and from these + callbacks everywhere else. + + +## Save Limit + +A user may save a capped number of messages across all conversations. Read the cap from app settings rather than hard-coding it. + + + +```typescript +let limit: number | null = await CometChat.getSaveMessageLimit(); + +if (limit !== null && savedCount >= limit) { + // Disable the save control instead of letting the user hit the error +} +``` + + + + +```javascript +CometChat.getSaveMessageLimit().then((limit) => { + if (limit !== null && savedCount >= limit) { + // Disable the save control instead of letting the user hit the error + } +}); +``` + + + + + +It resolves to `null` when the app settings carry no value — show generic copy in that case rather than guessing a number. + +## Feature Availability + + + +```typescript +let enabled: boolean = await CometChat.isSaveMessageEnabled(); + +if (enabled) { + // Show the Save option +} +``` + + + + +```javascript +CometChat.isSaveMessageEnabled().then((enabled) => { + if (enabled) { + // Show the Save option + } +}); +``` + + + + + +This resolves `false` rather than rejecting when the flag is missing or settings are unavailable. + +--- + +## Next Steps + + + + Highlight a message for everyone in a conversation + + + Pin a conversation to the top of the list + + + Fetch and order the conversation list + + + Filter messages by saved, pinned, type, tags and more + + diff --git a/sdk/react-native/thread-subscription.mdx b/sdk/react-native/thread-subscription.mdx new file mode 100644 index 000000000..f63f1ced7 --- /dev/null +++ b/sdk/react-native/thread-subscription.mdx @@ -0,0 +1,347 @@ +--- +title: "Thread Subscription" +sidebarTitle: "Thread Subscription" +description: "Subscribe and unsubscribe from message threads, read subscription state off a message, and fetch participated threads with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Subscribe / unsubscribe — a thread is identified by its PARENT message id +await CometChat.subscribeToThread(100); +await CometChat.unsubscribeFromThread(100); + +// Read state off a fetched parent message +const following = parentMessage.isThreadSubscribed(); + +// Align local copies after the server accepts (LOCAL ONLY — sends nothing) +parentMessage.setThreadSubscribed(true); + +// Thread inbox +const request = new CometChat.ThreadsRequestBuilder() + .setParticipatedByMe(true) + .setLimit(30) + .build(); +const threads = await request.fetchNext(); // CometChat.MessageThread[] +``` + + + +Thread subscription gives users control over thread noise. A user can **subscribe** to a message thread to be notified of future replies, or **unsubscribe** to mute it. + +The server subscribes a user to a thread automatically when they start it, reply in it, or are @-mentioned in it — and they can explicitly subscribe to any parent message, even one that has no replies yet. + + + Thread subscription builds on [Threaded + Messages](/sdk/react-native/threaded-messages). A thread is identified by the ID + of its **parent message** — there is no separate thread ID. + + +## How state works + +The SDK keeps **no subscription state of its own**. There is no cache and no listener to reconcile: + +- Every message fetch asks the server for the flag, and it arrives on the message — read it with `message.isThreadSubscribed()`. +- `subscribeToThread()` and `unsubscribeFromThread()` resolve when the server has accepted the change. The resolved promise **is** the acknowledgement. + +Your app owns the resulting UI state. That means you decide when to flip a toggle optimistically, and you decide what a thread's state is before you have fetched it. + +## Subscribe to a Thread + +Use `subscribeToThread()` with the ID of the thread's parent message. The call is **idempotent** — subscribing to a thread the user already follows succeeds silently. Subscribing to a message with zero replies is allowed; the user is notified when the first reply arrives. + + + +```typescript +let parentMessageId: number = 100; + +CometChat.subscribeToThread(parentMessageId).then( + (response: string) => { + // The server has accepted it — flip your toggle here. + console.log("Subscribed to thread:", response); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to subscribe:", error); + } +); +``` + + + + +```javascript +let parentMessageId = 100; + +CometChat.subscribeToThread(parentMessageId).then( + (response) => { + // The server has accepted it — flip your toggle here. + console.log("Subscribed to thread:", response); + }, + (error) => { + console.log("Failed to subscribe:", error); + } +); +``` + + + + + +## Unsubscribe from a Thread + +Use `unsubscribeFromThread()`. This is idempotent too — unsubscribing from a thread the user does not follow succeeds silently. + + + +```typescript +CometChat.unsubscribeFromThread(100).then( + (response: string) => { + console.log("Unsubscribed from thread:", response); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to unsubscribe:", error); + } +); +``` + + + + +```javascript +CometChat.unsubscribeFromThread(100).then( + (response) => { + console.log("Unsubscribed from thread:", response); + }, + (error) => { + console.log("Failed to unsubscribe:", error); + } +); +``` + + + + + + + Unsubscribing **hard-deletes** the server row, so a thread inbox built with + `ThreadsRequest` must drop that row rather than mark it unfollowed. It is also + **not sticky**: replying in the thread, or being @-mentioned in it, + re-subscribes the user. + + +## Read the Subscription State + + + +```typescript +if (parentMessage.isThreadSubscribed()) { + // Show the "Unfollow" affordance +} else { + // Show the "Follow" affordance +} +``` + + + + +```javascript +if (parentMessage.isThreadSubscribed()) { + // Show the "Unfollow" affordance +} else { + // Show the "Follow" affordance +} +``` + + + + + +Every message fetch the SDK makes asks the server for this flag, so any message you obtained from `MessagesRequest` or `getMessageDetails()` carries it. + + + A message delivered over the **socket** carries no flag and therefore reads + `false`. That is not a claim that the user is unsubscribed — it means nobody + asked. When you need certainty for a thread you have not fetched (a deep link, + for instance), fetch the parent message with `CometChat.getMessageDetails()` + and read the flag off the result. + + +### You are subscribed to your own messages + +Sending a message subscribes you to the thread it may later grow — there is nothing to call. The message comes back with `threadSubscribed: true`, both in the send response and on later fetches, and **only for you**: the flag is per-viewer, so the same message reads `false` for everybody else until they subscribe themselves. + +That default is what makes the flag meaningful on your own messages. Since it starts out `true`, a `false` on a message **you sent** — read from a fetch, not the socket — is not silence. It means you unsubscribed, and nothing should quietly put you back. + +This only holds for a message you sent and obtained from a fetch. On anyone else's message, or on anything socket-delivered, `false` still just means the server was not asked. + +### Keeping your own copies in sync + +The same thread can be represented by several message objects at once — a row in the message list, the header of an open thread view, an entry in a thread inbox. Because the SDK caches nothing, use `setThreadSubscribed()` to align the copies you hold once you know the answer: + + + +```typescript +await CometChat.subscribeToThread(100); +// The server accepted it — bring the objects you are rendering into line. +parentMessage.setThreadSubscribed(true); +``` + + + + +```javascript +CometChat.subscribeToThread(100).then(() => { + // The server accepted it — bring the objects you are rendering into line. + parentMessage.setThreadSubscribed(true); +}); +``` + + + + + + + `setThreadSubscribed()` is **local only** — it changes the object in memory and + sends nothing to the server. Use `subscribeToThread()` / + `unsubscribeFromThread()` to change the actual subscription. + + +## Reacting to Replies + +A thread reply is an **ordinary message** with `parentMessageId` set, delivered through the standard `MessageListener` like any other message. There is no separate thread listener. + + + +```typescript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onTextMessageReceived: (message: CometChat.TextMessage) => { + const parentMessageId = message.getParentMessageId(); + if (parentMessageId) { + // A reply landed in a thread — bump your thread row here. + } + }, + }) +); +``` + + + + +```javascript +CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onTextMessageReceived: (message) => { + const parentMessageId = message.getParentMessageId(); + if (parentMessageId) { + // A reply landed in a thread — bump your thread row here. + } + }, + }) +); +``` + + + + + +Using `MessageListener` also gets you `onMessageEdited` and `onMessageDeleted` for replies, which a thread-only channel would not. + +Your own replies do not arrive on a listener — bump your thread row from the `sendMessage()` promise instead. + +## Fetch the Threads a User Participates In + +Use `ThreadsRequest` to build a thread inbox. Every returned thread is one the logged-in user is subscribed to. + + + +```typescript +let threadsRequest: CometChat.ThreadsRequest = + new CometChat.ThreadsRequestBuilder() + .setParticipatedByMe(true) + .setLimit(30) + .build(); + +threadsRequest.fetchNext().then( + (threads: CometChat.MessageThread[]) => { + console.log("Threads fetched:", threads); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to fetch threads:", error); + } +); +``` + + + + +```javascript +let threadsRequest = new CometChat.ThreadsRequestBuilder() + .setParticipatedByMe(true) + .setLimit(30) + .build(); + +threadsRequest.fetchNext().then( + (threads) => { + console.log("Threads fetched:", threads); + }, + (error) => { + console.log("Failed to fetch threads:", error); + } +); +``` + + + + + +Scope the inbox to a single conversation with `setUid()` or `setGuid()`. Call `fetchNext()` again on the same object to page, and check `hasMore()` before doing so. + + + `ThreadsRequestBuilder` spells these `setUid()` / `setGuid()` — **not** the + `setUID()` / `setGUID()` used by `MessagesRequestBuilder`. The two are + mutually exclusive; set at most one. + + +### Reading a thread row + +Each row is a `MessageThread` — enough to render an inbox without fetching the parent message separately. + +| Method | Returns | +| ------------------------- | ----------------------------------------------------------------------- | +| `getParentMessageId()` | The thread's ID — pass it to `subscribeToThread()`. | +| `getParentMessage()` | The parent message, hydrated. | +| `getLastReply()` | The most recent reply, for the preview line. | +| `getReplyCount()` | Total replies in the thread. | +| `getUnreadReplyCount()` | Replies the user has not read — the badge count. | +| `getUpdatedAt()` | When the thread last changed, for ordering the inbox. | +| `getConversationId()` | The conversation the thread lives in. | +| `getReceiverId()` / `getReceiverType()` | Where to route a tap on the row. | +| `isSubscribed()` | Always `true` for rows from a `setParticipatedByMe(true)` fetch — presence in the list *is* the subscription. | + + + `getUnreadReplyCount()` returns `null`, not `0`, when the count is unknown. + Treat `null` as "no badge" rather than as zero unread. + + +--- + +## Next Steps + + + + Send and fetch replies inside a thread + + + @-mentions, which auto-subscribe a user to a thread + + + Every listener the SDK exposes, in one place + + + Bookmark a message privately, across conversations + + diff --git a/sdk/react-native/threaded-messages.mdx b/sdk/react-native/threaded-messages.mdx index 9e58b2928..9352bbb81 100644 --- a/sdk/react-native/threaded-messages.mdx +++ b/sdk/react-native/threaded-messages.mdx @@ -190,6 +190,75 @@ messagesRequest.fetchPrevious().then( The `fetchPrevious()` method returns an array of [`BaseMessage`](/sdk/reference/messages#basemessage) objects representing thread replies. +### Hydrate the Parent Message + +A thread reply carries its `parentMessageId`, but not the parent message itself. Use `withParent(true)` when you need the parent hydrated alongside each reply — for example, to render a saved or pinned reply with the message it answers. + + + +```typescript +let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setLimit(30) + .setParentMessageId(100) + .withParent(true) + .build(); +``` + + + + +```javascript +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setLimit(30) + .setParentMessageId(100) + .withParent(true) + .build(); +``` + + + + + +`withParent(true)` works on any message fetch, including the pinned and saved lists. + +## Subscribe to a Thread + +Users can follow a thread to be notified of future replies, or unfollow it to mute the noise. The server subscribes them automatically when they start a thread, reply in it, or are @-mentioned in it, and they can subscribe explicitly to any parent message. Read the current state off the parent message with `isThreadSubscribed()`. + + + +```typescript +CometChat.subscribeToThread(100).then( + (response: string) => { + console.log("Subscribed to thread:", response); + }, + (error: CometChat.CometChatException) => { + console.log("Failed to subscribe:", error); + } +); +``` + + + + +```javascript +CometChat.subscribeToThread(100).then( + (response) => { + console.log("Subscribed to thread:", response); + }, + (error) => { + console.log("Failed to subscribe:", error); + } +); +``` + + + + + +See [Thread Subscription](/sdk/react-native/thread-subscription) for unsubscribing, reading the subscription state, and fetching the threads a user participates in. + ## Avoid Threaded Messages in User/Group Conversations Use `hideReplies(true)` to exclude threaded messages when fetching messages for a conversation. diff --git a/ui-kit/react-native/conversations.mdx b/ui-kit/react-native/conversations.mdx index 4831585f6..e2c86e498 100644 --- a/ui-kit/react-native/conversations.mdx +++ b/ui-kit/react-native/conversations.mdx @@ -40,6 +40,7 @@ description: "Display CometChat React Native UI Kit conversations with last mess "visibility": { "hideBackButton": { "type": "boolean", "default": true }, "hideHeader": { "type": "boolean", "default": false }, + "pinConversationOptionVisibility": { "type": "boolean", "default": true }, "hideError": { "type": "boolean", "default": false }, "receiptsVisibility": { "type": "boolean", "default": true }, "hideSubmitButton": { "type": "boolean", "default": true }, @@ -410,6 +411,122 @@ Manual: deleting a conversation via the SDK directly (not through the component' --- +## Pinning Conversations + +A pinned conversation sits at the top of the list. The pin is **private to the logged-in user** — +nobody else sees it — and it syncs to that user's other devices. + + + **Off by default, and it needs a server flag that is not seeded.** Pin Conversation requires + `features.ux.conversations.pinned.enabled`, which ships enabled in no plan and must be mapped per + app. Until then every attempt rejects with `ERR_FEATURE_NOT_ACCESSIBLE`. On top of that, the kit + needs your own opt-in via `PinConversationConfig.enable(true)`. + + + + +```typescript +import { PinConversationConfig } from "@cometchat/chat-uikit-react-native"; + +// Once, at app start — defaults to off +PinConversationConfig.enable(true); +``` + + + + +```javascript +import { PinConversationConfig } from "@cometchat/chat-uikit-react-native"; + +PinConversationConfig.enable(true); +``` + + + + + +`pinConversationOptionVisibility` (default `true`) hides the option on one particular list. It is +**ANDed** with the global config, so the option renders only when both allow it. + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +### Reading pin state + + + +```typescript +import { + isConversationPinned, + isSystemPinnedConversation, +} from "@cometchat/chat-uikit-react-native"; + +if (isConversationPinned(conversation)) { + // Pinned — render the pin affordance +} + +if (isSystemPinnedConversation(conversation)) { + // An admin/global pin. A user cannot unpin one of these, + // so hide or disable the unpin control. +} +``` + + + + +```javascript +import { + isConversationPinned, + isSystemPinnedConversation, +} from "@cometchat/chat-uikit-react-native"; + +if (isConversationPinned(conversation)) { + // Pinned — render the pin affordance +} + +if (isSystemPinnedConversation(conversation)) { + // Admin/global pin — not user-unpinnable +} +``` + + + + + + + The conversation list arrives **already pin-ordered** from the server — admin pins first, then the + user's own, then everything else — and each row carries its pin attributes. You do not need a + separate fetch to render a pinned section. + + + + **Conversation pins have no real-time channel.** The **Chat SDK** exposes no `ConversationListener` + and no `onConversationPinned` / `onConversationUnpinned` callbacks, so a pin made on another device + never arrives over the socket. The list updates from the action taken on this device; re-fetch when + the screen regains focus to pick up pins made elsewhere. + + Do not confuse this with `CometChatUIEventHandler.addConversationListener`, used above — that is the + **UI Kit's own event bus** for in-app events such as `ccConversationDeleted`, and it is unrelated to + the SDK listener that does not exist. + + +See [Pin A Conversation](/sdk/react-native/pin-conversation) for the SDK methods underneath. + + ## Custom View Slots Each slot replaces a section of the default UI. Slots that accept a conversation parameter receive the `CometChat.Conversation` object for that row. diff --git a/ui-kit/react-native/message-list.mdx b/ui-kit/react-native/message-list.mdx index cac658ca5..df3f7a37b 100644 --- a/ui-kit/react-native/message-list.mdx +++ b/ui-kit/react-native/message-list.mdx @@ -43,6 +43,11 @@ description: "Display sent and received messages with text, media, reactions, re "hideTimestamp": { "type": "boolean", "default": false }, "hideReplyOption": { "type": "boolean", "default": false }, "hideReplyInThreadOption": { "type": "boolean", "default": false }, + "hideThreadSubscriptionOption": { "type": "boolean", "default": false }, + "hidePinMessageOption": { "type": "boolean", "default": false }, + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, "hideShareMessageOption": { "type": "boolean", "default": false }, "hideEditMessageOption": { "type": "boolean", "default": false }, "hideDeleteMessageOption": { "type": "boolean", "default": false }, @@ -395,6 +400,72 @@ The component listens to these SDK events internally. No manual attachment neede --- +## Pin, Save and Thread Subscription Options + +These actions appear in the message options sheet, each with a prop to hide it. + +| Prop | Default | Hides | +| --- | --- | --- | +| `hidePinMessageOption` | `false` | Pin message | +| `hideUnpinMessageOption` | `false` | Unpin message | +| `hideSaveMessageOption` | `false` | Save message | +| `hideUnsaveMessageOption` | `false` | Unsave message | +| `hideThreadSubscriptionOption` | `false` | Follow / Unfollow thread | + + + **A `false` default does not mean the option is visible.** Pin, Save and Pin Conversation are also + gated on your app's CometChat Dashboard settings. The UI Kit resolves those flags at login and on + every reconnect, so an option renders only when the Dashboard allows it *and* the prop does not + hide it. There is no start-up call to make. + + Thread subscription is different: it is not an app setting, so `ThreadSubscriptionConfig` is a real + switch — and it defaults to **on**, matching the React UI Kit. The option shows unless you opt out. + + + + +```typescript +import { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native"; + +// Thread subscription is on by default — call this only to opt OUT. +// Pin and Save need no equivalent: the kit reads their Dashboard flags at login. +ThreadSubscriptionConfig.setEnabled(false); +``` + + + + +```javascript +import { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native"; + +ThreadSubscriptionConfig.setEnabled(false); +``` + + + + + +The Dashboard flag and the per-instance prop are **ANDed**: the Dashboard decides whether a feature is +available to your app at all, and these props hide it on one particular list. + + + **Removing asks, adding does not.** Unpin and Unsave show a confirmation; Pin and Save run + immediately. The asymmetry is deliberate and shared across all CometChat UI Kits — the additive + action has its own undo one tap away, the destructive one does not. + + + + **A just-sent message may briefly refuse to pin or save.** On a moderated app the server holds a new + message at `moderation.status: "pending"` for a second or two and refuses with + `403 ERR_MESSAGE_NO_ACCESS` — the same code it returns for a genuine permission refusal. The kit + keeps the options visible and shows a transient message rather than claiming the user lacks + permission, because by the time someone opens the sheet and taps, moderation has usually finished. + + +See [Pinned Messages](/ui-kit/react-native/pinned-messages) and +[Saved Messages](/ui-kit/react-native/saved-messages) for the panels that list the results. + + ## Custom View Slots Each slot replaces a section of the default UI. @@ -1017,6 +1088,17 @@ Hides the moderation status UI. --- +### hidePinMessageOption + +Hides the "Pin message" option. Pin Messages must also be enabled for your app in the CometChat Dashboard before the option can appear at all. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideReactionOption Hides the reaction option. @@ -1050,6 +1132,17 @@ Hides the reply option. --- +### hideSaveMessageOption + +Hides the "Save message" option. Save Messages must also be enabled for your app in the CometChat Dashboard before the option can appear at all. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideShareMessageOption Hides the share message option. @@ -1072,6 +1165,17 @@ Hides suggested messages in AI view. --- +### hideThreadSubscriptionOption + +Hides the "Follow / Unfollow thread" option. Unlike pin and save, `ThreadSubscriptionConfig` defaults to **on**, so this prop is the usual way to hide it. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideTimestamp Hides timestamps on messages. @@ -1083,6 +1187,28 @@ Hides timestamps on messages. --- +### hideUnpinMessageOption + +Hides the "Unpin message" option. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnsaveMessageOption + +Hides the "Unsave message" option. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideTranslateMessageOption Hides the translate message option. diff --git a/ui-kit/react-native/pinned-messages.mdx b/ui-kit/react-native/pinned-messages.mdx new file mode 100644 index 000000000..62e46eafb --- /dev/null +++ b/ui-kit/react-native/pinned-messages.mdx @@ -0,0 +1,301 @@ +--- +title: "Pinned Messages" +sidebarTitle: "Pinned Messages" +description: "Display the messages pinned in a conversation, with unpin, save, copy, share and info actions." +--- + +{/* TL;DR for Agents and Quick Reference */} + +```json +{ + "component": "CometChatPinnedMessages", + "package": "@cometchat/chat-uikit-react-native", + "import": "import { CometChatPinnedMessages } from \"@cometchat/chat-uikit-react-native\";", + "description": "Lists the messages pinned in a conversation, newest pin first. A pin is conversation-wide and visible to everyone.", + "requires": { + "dashboardFlag": "Pin Messages must be enabled for your app in the CometChat Dashboard", + "resolution": "The kit reads the flag at login and on every reconnect — no app code required. Force a re-read with refreshPinSaveFeatures()" + }, + "props": { + "data": { + "user": { "type": "CometChat.User", "default": "undefined", "note": "Scopes the panel to a one-on-one conversation. Mutually exclusive with group" }, + "group": { "type": "CometChat.Group", "default": "undefined", "note": "Scopes the panel to a group. Mutually exclusive with user" }, + "limit": { "type": "number", "default": 30, "note": "Page size for the fetch; the SDK caps it at 100" } + }, + "callbacks": { + "onBack": "() => void", + "onItemPress": "(message: CometChat.BaseMessage) => void" + }, + "visibility": { + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, + "hideCopyMessageOption": { "type": "boolean", "default": false }, + "hideShareMessageOption": { "type": "boolean", "default": false }, + "hideMessageInfoOption": { "type": "boolean", "default": false } + }, + "customization": { + "ItemView": "(message: CometChat.BaseMessage) => JSX.Element", + "title": { "type": "string", "default": "localized" }, + "style": "DeepPartial" + } + }, + "helpers": { + "isPinned": "(message) => boolean", + "isSystemPin": "(message) => boolean — pinnedBy is SYSTEM_PINNER (\"app_system\")" + } +} +``` + + +`CometChatPinnedMessages` lists the messages pinned in one conversation, newest pin first. A pin is +conversation-wide and visible to everyone, so this panel shows the same set to every participant. + + + **Pin Messages must be enabled for your app in the CometChat Dashboard.** Until it is, the pin + options never render and this panel has nothing to show. The UI Kit reads that flag itself at + login and on every reconnect — there is no app code to write. + + +## Where It Fits + +Open it from the message header or an overflow menu, scoped to the conversation the user is in. It is +a full-screen panel with its own back affordance rather than an inline strip. + +## Minimal Render + +Pass **exactly one** of `user` or `group` — the panel is scoped to a single conversation. + + + +```tsx +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage) => scrollToMessage(message)} +/> +``` + + + + +```tsx +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage) => scrollToMessage(message)} +/> +``` + + + + + +## Enabling the feature + +Pin and Save are gated on your app's Dashboard settings. The UI Kit resolves those flags at login +and on every reconnect and switches the options on or off accordingly, so there is nothing to call +at start-up. + +`PinSaveConfig` is exported for the cases where you need to read or override that resolved state — +not as a switch you must throw. + + + +```typescript +import { + PinSaveConfig, + refreshPinSaveFeatures, + getPinSaveFeatures, +} from "@cometchat/chat-uikit-react-native"; + +// Read what the kit resolved from the Dashboard flags +const features = getPinSaveFeatures(); + +// Force a re-read after flipping a flag in the dashboard, without a re-login +await refreshPinSaveFeatures(); +``` + + + + +```javascript +import { + PinSaveConfig, + refreshPinSaveFeatures, + getPinSaveFeatures, +} from "@cometchat/chat-uikit-react-native"; + +const features = getPinSaveFeatures(); +refreshPinSaveFeatures(); +``` + + + + + + + You do not need to fetch these flags yourself, and you do not need to enable the feature in code. + The kit resolves them at login and after every reconnect. `refreshPinSaveFeatures()` exists for the + case where an admin flips a flag in the Dashboard while the app is running. + + +## Props + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `user` | `CometChat.User` | — | Scopes the panel to a one-on-one conversation. Mutually exclusive with `group`. | +| `group` | `CometChat.Group` | — | Scopes the panel to a group. Mutually exclusive with `user`. | +| `limit` | `number` | `30` | Page size for the fetch. The SDK rejects a value above 100. | +| `onBack` | `() => void` | — | Called when the back affordance is pressed. | +| `onItemPress` | `(message) => void` | — | Called when a row is pressed — use it to jump to the message in the list. | +| `hideUnpinMessageOption` | `boolean` | `false` | Hides Unpin in the row menu. | +| `hideSaveMessageOption` | `boolean` | `false` | Hides Save in the row menu. | +| `hideUnsaveMessageOption` | `boolean` | `false` | Hides Unsave in the row menu. | +| `hideCopyMessageOption` | `boolean` | `false` | Hides Copy. | +| `hideShareMessageOption` | `boolean` | `false` | Hides Share. | +| `hideMessageInfoOption` | `boolean` | `false` | Hides Message Info. | +| `ItemView` | `(message) => JSX.Element` | — | Replaces the default row entirely. | +| `title` | `string` | localized | Panel title. | +| `style` | `DeepPartial` | — | Style overrides. | + +## Reading pin state yourself + +The kit exports helpers so a custom `ItemView` does not have to reimplement them. The SDK has no +`isPinned()` of its own — these are UI Kit conveniences over `getPinnedAt()` and `getPinnedBy()`. + + + +```typescript +import { isPinned, isSystemPin, SYSTEM_PINNER } from "@cometchat/chat-uikit-react-native"; + +if (isPinned(message)) { + // Pinned by somebody in this conversation +} + +if (isSystemPin(message)) { + // An admin/global pin — pinnedBy is SYSTEM_PINNER ("app_system"), not a real member +} +``` + + + + +```javascript +import { isPinned, isSystemPin } from "@cometchat/chat-uikit-react-native"; + +if (isPinned(message)) { + // Pinned by somebody in this conversation +} + +if (isSystemPin(message)) { + // An admin/global pin +} +``` + + + + + +## Actions and Events + +The panel keeps itself current from the kit's event bus, so a pin made anywhere — this screen, the +message list, or another participant's device — lands here without a manual refresh. + +| Event | Fires when | +| --- | --- | +| `ccMessagePinned` / `ccMessageUnpinned` | This device pinned or unpinned. | +| `ccMessageSaved` / `ccMessageUnsaved` | This device saved or unsaved. | +| `onMessagePinned` / `onMessageUnpinned` | Another participant pinned or unpinned — broadcast to everyone. | +| `onMessageSaved` / `onMessageUnsaved` | The same user saved on another device — private multi-device. | + + + **Removing asks, adding does not.** Unpin and Unsave show a confirmation; Pin and Save run + immediately. This is deliberate and consistent across all CometChat UI Kits — the additive action + has its own undo one tap away, the destructive one does not. + + +## Common Patterns + +### Jump to the pinned message + + + +```tsx + { + navigation.goBack(); + scrollToMessageId(message.getId()); + }} +/> +``` + + + + +```jsx + { + navigation.goBack(); + scrollToMessageId(message.getId()); + }} +/> +``` + + + + + +### Read-only panel + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +--- + +## Next Steps + + + + The private, cross-conversation counterpart + + + Where the Pin and Save options are raised + + + The SDK methods underneath this component + + + Every UI Kit event, in one place + + diff --git a/ui-kit/react-native/saved-messages.mdx b/ui-kit/react-native/saved-messages.mdx new file mode 100644 index 000000000..9a6199556 --- /dev/null +++ b/ui-kit/react-native/saved-messages.mdx @@ -0,0 +1,259 @@ +--- +title: "Saved Messages" +sidebarTitle: "Saved Messages" +description: "Display the logged-in user's saved messages across every conversation, with unsave and jump-to-message actions." +--- + +{/* TL;DR for Agents and Quick Reference */} + +```json +{ + "component": "CometChatSavedMessages", + "package": "@cometchat/chat-uikit-react-native", + "import": "import { CometChatSavedMessages } from \"@cometchat/chat-uikit-react-native\";", + "description": "Lists every message the logged-in user has saved, newest save first. A save is private and spans all conversations, so there is no user or group prop.", + "requires": { + "dashboardFlag": "Save Messages must be enabled for your app in the CometChat Dashboard", + "resolution": "The kit reads the flag at login and on every reconnect — no app code required. Force a re-read with refreshPinSaveFeatures()" + }, + "props": { + "data": { + "limit": { "type": "number", "default": 30, "note": "Page size for the fetch; the SDK caps it at 100" } + }, + "callbacks": { + "onBack": "() => void", + "onItemPress": "(message: CometChat.BaseMessage, source: SavedMessageSource | null) => void" + }, + "visibility": { + "hideUnsaveMessageOption": { "type": "boolean", "default": false } + }, + "customization": { + "ItemView": "(message: CometChat.BaseMessage) => JSX.Element", + "title": { "type": "string", "default": "localized" }, + "style": "DeepPartial" + } + }, + "helpers": { + "isSaved": "(message) => boolean — savedAt is per-viewer, always about the logged-in user" + } +} +``` + + +`CometChatSavedMessages` lists every message the logged-in user has saved, newest save first. Unlike a +pin, a save is **private and spans conversations** — nobody else can see it, and the list is not scoped +to a single chat. + + + **Save Messages must be enabled for your app in the CometChat Dashboard.** Until it is, the Save + option never renders and this panel has nothing to show. The UI Kit reads that flag itself at login + and on every reconnect — there is no app code to write. + + +## Where It Fits + +Because the list is account-wide, this panel belongs at app level — a tab, a drawer entry, or a +profile screen — not inside a single conversation. + +## Minimal Render + +There is deliberately no `user` or `group` prop. Scoping it to one conversation would defeat the point. + + + +```tsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage, source) => { + // `source` carries the conversation the message came from + openConversation(message, source); + }} +/> +``` + + + + +```jsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message, source) => { + openConversation(message, source); + }} +/> +``` + + + + + +## Props + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `limit` | `number` | `30` | Page size for the fetch. The SDK rejects a value above 100. | +| `onBack` | `() => void` | — | Called when the back affordance is pressed. | +| `onItemPress` | `(message, source) => void` | — | Called when a row is pressed. `source` identifies which conversation the message belongs to. | +| `hideUnsaveMessageOption` | `boolean` | `false` | Hides Unsave in the row menu. | +| `ItemView` | `(message) => JSX.Element` | — | Replaces the default row entirely. | +| `title` | `string` | localized | Panel title. | +| `style` | `DeepPartial` | — | Style overrides. | + +## Routing a tap back to its conversation + +Because the rows come from different conversations, `onItemPress` hands you a `source` alongside the +message. If you build a custom `ItemView`, the message itself carries the same context. + + + +```tsx + { + const conversationId = message.getConversationId(); + const receiverType = message.getReceiverType(); // "user" or "group" + const receiverId = message.getReceiverId(); + + navigateToConversation({ receiverType, receiverId, conversationId }); + }} +/> +``` + + + + +```jsx + { + navigateToConversation({ + receiverType: message.getReceiverType(), + receiverId: message.getReceiverId(), + conversationId: message.getConversationId(), + }); + }} +/> +``` + + + + + +## Reading save state yourself + + + +```typescript +import { isSaved } from "@cometchat/chat-uikit-react-native"; + +if (isSaved(message)) { + // The logged-in user saved this one +} +``` + + + + +```javascript +import { isSaved } from "@cometchat/chat-uikit-react-native"; + +if (isSaved(message)) { + // The logged-in user saved this one +} +``` + + + + + + + `savedAt` is **per-viewer**. The same message reads saved for the user who saved it and unsaved for + everybody else — so `isSaved()` is always a statement about the logged-in user, never about the + message globally. + + +## Actions and Events + +| Event | Fires when | +| --- | --- | +| `ccMessageSaved` / `ccMessageUnsaved` | This device saved or unsaved. | +| `onMessageSaved` / `onMessageUnsaved` | The same user saved on **another** device. | + + + The SDK listener never fires on the device that performed the save — that device already has the + resolved message. The panel updates from the local event there, and from the SDK event everywhere + else, so a save on a phone appears on a tablet without a refresh. + + +## Common Patterns + +### A saved-messages tab + + + +```tsx + ( + openConversation(message)} + /> + )} +/> +``` + + + + +```jsx + ( + openConversation(message)} /> + )} +/> +``` + + + + + +### Read-only list + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +--- + +## Next Steps + + + + The conversation-wide, everyone-sees-it counterpart + + + Where the Save option is raised + + + The SDK methods underneath this component + + + Every UI Kit event, in one place + +