@adminide-stack/yantra-mobile 12.0.105 → 12.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/lib/features/messaging/BlockedContactsScreen.js +127 -0
  2. package/lib/features/messaging/BlockedContactsScreen.js.map +1 -0
  3. package/lib/features/messaging/ChatAttachments.js +4 -2
  4. package/lib/features/messaging/ChatAttachments.js.map +1 -1
  5. package/lib/features/messaging/ChatComponents.js +194 -10
  6. package/lib/features/messaging/ChatComponents.js.map +1 -1
  7. package/lib/features/messaging/ChatInfoScreen.js +173 -17
  8. package/lib/features/messaging/ChatInfoScreen.js.map +1 -1
  9. package/lib/features/messaging/ConversationListScreen.js +17 -1
  10. package/lib/features/messaging/ConversationListScreen.js.map +1 -1
  11. package/lib/features/messaging/MediaGalleryScreen.js +264 -0
  12. package/lib/features/messaging/MediaGalleryScreen.js.map +1 -0
  13. package/lib/features/messaging/NativeChatExperience.js +137 -18
  14. package/lib/features/messaging/NativeChatExperience.js.map +1 -1
  15. package/lib/features/messaging/NewGroupScreen.js +191 -0
  16. package/lib/features/messaging/NewGroupScreen.js.map +1 -0
  17. package/lib/features/messaging/PollComposer.js +137 -0
  18. package/lib/features/messaging/PollComposer.js.map +1 -0
  19. package/lib/features/messaging/PollVotesScreen.js +122 -0
  20. package/lib/features/messaging/PollVotesScreen.js.map +1 -0
  21. package/lib/features/messaging/ThreadScreen.js +180 -13
  22. package/lib/features/messaging/ThreadScreen.js.map +1 -1
  23. package/lib/features/messaging/chatLinks.js +39 -2
  24. package/lib/features/messaging/chatLinks.js.map +1 -1
  25. package/lib/features/messaging/chatModel.js +186 -19
  26. package/lib/features/messaging/chatModel.js.map +1 -1
  27. package/lib/features/messaging/chatOperations.js +111 -2
  28. package/lib/features/messaging/chatOperations.js.map +1 -1
  29. package/lib/features/messaging/nativeChatChannelQuery.js +7 -2
  30. package/lib/features/messaging/nativeChatChannelQuery.js.map +1 -1
  31. package/lib/features/messaging/useChatData.js +248 -10
  32. package/lib/features/messaging/useChatData.js.map +1 -1
  33. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"chatOperations.js","sources":["../../../src/features/messaging/chatOperations.ts"],"sourcesContent":["/**\n * Every GraphQL operation the chat remote sends, as plain strings.\n *\n * Strings, not `gql` documents, because the remote must not import\n * @apollo/client or graphql - the host parses and executes these on the app's\n * own client (see `ChatGraphQLSeam`). The upside beyond bundle size is that the\n * QUERY SHAPE ships over the air: a missing field or a wrong argument is a\n * surface deploy, not an App Store release.\n *\n * HARD REQUIREMENT - every operation is NAMED, and every subscription's name\n * ends in `_WS`. `portable-devices/mobile/src/config/base-apollo-client.ts`\n * routes ws-vs-http with `operationName.endsWith('_WS')`; an anonymous\n * operation makes that `undefined.endsWith(...)`, which throws for EVERY\n * operation the app sends, not just this one. `chatOperations.test.ts` asserts\n * both properties over every export in this file, so a new operation cannot\n * quietly break the app's transport.\n */\n\n/**\n * Shared field set for a message.\n *\n * `author` and `creator` are both selected on purpose: messenger-box populates\n * `author` on some write paths and `creator` on others (`sendMessage` sets\n * creator), and a transcript that reads only one shows \"Unknown\" for half the\n * messages. `chatModel.pickAuthor` collapses them.\n */\nexport const POST_FIELDS = `\n id\n message\n type\n createdAt\n updatedAt\n editedAt\n deletedAt\n rootId\n parentId\n filenames\n props\n author { id givenName familyName username email picture }\n creator { id givenName familyName username email picture }\n reactionSummary { reaction count userIds }\n viewerStarred\n`;\n\n/**\n * A message plus its attached files, for the transcript (not the chat list, where resolving\n * files per row would cost a lookup per conversation - its preview uses `props.attachmentKind`).\n */\nexport const MESSAGE_FIELDS = `\n ${POST_FIELDS}\n files { data { id name mimeType url width height size } }\n`;\n\nconst CHANNEL_FIELDS = `\n id\n type\n orgName\n title\n displayName\n topic\n lastPostAt\n totalMsgCount\n createdAt\n lastMessage { ${POST_FIELDS} }\n creator { id givenName familyName username email picture }\n members { id user { id givenName familyName username email picture } msgCount lastViewedAt lastDeliveredAt }\n viewerMutedUntil\n viewerPinnedAt\n viewerArchivedAt\n viewerDraft\n`;\n\n/**\n * The conversation list.\n *\n * `criteria.members.$elemMatch.user` is NOT sent: the server derives membership\n * from the session (`accountId`) and a client-supplied member filter is exactly\n * the kind of thing that lets a caller read someone else's list. Org scoping is\n * likewise server-side now (`channelOrgScope`). Native Messages passes\n * `criteria: { type: DIRECT, orgName: yantra-chat }` via `nativeChatChannelQuery`.\n */\nexport const CHANNELS_QUERY = `\n query ChatChannels($limit: Int, $skip: Int, $criteria: AnyObject, $sort: Sort) {\n channelsByUser(limit: $limit, skip: $skip, criteria: $criteria, sort: $sort) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/**\n * One page of a transcript, newest first.\n *\n * Paged with `skip`, which is what the server exposes. That is offset paging and\n * it can repeat or drop a row if messages arrive mid-scroll; `chatModel.mergePosts`\n * de-duplicates by id so the visible effect is bounded. A cursor (`before: id`)\n * would be strictly better and is worth adding server-side later.\n */\nexport const MESSAGES_QUERY = `\n query ChatMessages($channelId: ID, $limit: Int! = 30, $skip: Int! = 0, $sort: Sort) {\n messages(channelId: $channelId, limit: $limit, skip: $skip, sort: $sort) {\n totalCount\n data {\n ${MESSAGE_FIELDS}\n }\n }\n }\n`;\n\nexport const SEND_MESSAGE_MUTATION = `\n mutation ChatSendMessage($channelId: String!, $messageInput: MessageInput!, $postId: ID) {\n sendMessage(channelId: $channelId, messageInput: $messageInput, postId: $postId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/**\n * Attachments, step 1: one presigned S3 PUT URL per file for a post that does not exist yet\n * (the client picks the post id; `sendMessage` then creates the post with that id).\n */\nexport const CREATE_MESSAGE_UPLOAD_LINKS_MUTATION = `\n mutation ChatCreateMessageUploadLinks($postId: ID!, $filenames: [String!]!) {\n createMessageFilesUploadLink(postId: $postId, filenames: $filenames)\n }\n`;\n\n/** Attachments, step 3 (after the PUTs): record the uploaded files against the post id. */\nexport const ATTACH_UPLOADED_FILES_MUTATION = `\n mutation ChatAttachUploadedFiles($postId: ID!, $files: [UploadedFileInput!]!) {\n attachUploadedFilesToMessage(postId: $postId, files: $files) {\n id\n name\n mimeType\n url\n width\n height\n }\n }\n`;\n\n/**\n * Who has read how far in one chat - just the members' `lastViewedAt`. Polled while\n * the viewer's newest message is still unread, so its ticks turn blue without a reload.\n */\nexport const CHANNEL_READ_STATE_QUERY = `\n query ChatChannelReadState($id: String) {\n viewChannelDetail(id: $id) {\n id\n members { id user { id } lastViewedAt lastDeliveredAt }\n }\n }\n`;\n\n/**\n * This phone has the channel's messages up to now - the senders' two gray ticks. Called when a\n * message arrives (live or with the list), never more than once per chat every few seconds.\n */\nexport const MARK_CHANNEL_DELIVERED_MUTATION = `\n mutation ChatMarkChannelDelivered($channelId: ID!) {\n markChannelDelivered(channelId: $channelId)\n }\n`;\n\n/** Pin to the top of, or archive out of, the viewer's own chat list. An omitted flag is left as is. */\nexport const SET_CHANNEL_LIST_STATE_MUTATION = `\n mutation ChatSetChannelListState($channelId: ID!, $pinned: Boolean, $archived: Boolean) {\n setChannelListState(channelId: $channelId, pinned: $pinned, archived: $archived)\n }\n`;\n\n/** Rewrite the viewer's own message; everyone else sees it through `chatMessageUpdated`. */\nexport const EDIT_MESSAGE_MUTATION = `\n mutation ChatEditMessage($messageId: MessageIdentifier!, $messageInput: MessageInput!) {\n editMessage(messageId: $messageId, messageInput: $messageInput) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** WhatsApp's \"Delete for everyone\" (own messages, within 48 hours): leaves a tombstone. */\nexport const DELETE_MESSAGE_FOR_EVERYONE_MUTATION = `\n mutation ChatDeleteMessageForEveryone($messageId: MessageIdentifier!) {\n deleteMessageForEveryone(messageId: $messageId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** Star or unstar a message for the viewer only. */\nexport const SET_MESSAGE_STARRED_MUTATION = `\n mutation ChatSetMessageStarred($messageId: MessageIdentifier!, $starred: Boolean!) {\n setMessageStarred(messageId: $messageId, starred: $starred)\n }\n`;\n\n/** The viewer's starred messages, newest first, with the chat each one is in. */\nexport const STARRED_MESSAGES_QUERY = `\n query ChatStarredMessages($limit: Int, $skip: Int) {\n starredMessages(limit: $limit, skip: $skip) {\n ${MESSAGE_FIELDS}\n channel { id }\n }\n }\n`;\n\n/** Search one chat's messages (newest first). The server checks membership. */\nexport const SEARCH_MESSAGES_QUERY = `\n query ChatSearchMessages($query: String!, $channelId: ID, $limit: Int, $skip: Int) {\n searchMessages(query: $query, channelId: $channelId, limit: $limit, skip: $skip) {\n totalCount\n data {\n ${POST_FIELDS}\n }\n }\n }\n`;\n\n/** \"typing…\": start while composing, stop on send or after a pause. */\nexport const SEND_TYPING_MUTATION = `\n mutation ChatSendTyping($channelId: String!, $action: String!) {\n sendTypingIndicator(channelId: $channelId, action: $action)\n }\n`;\n\n/** Who is typing in a chat. `_WS` suffix is load-bearing - see the file header. */\nexport const TYPING_SUBSCRIPTION_WS = `\n subscription ChatTyping_WS($channelId: String!) {\n typingIndicator(channelId: $channelId) {\n userId\n displayName\n channelId\n action\n }\n }\n`;\n\n/** A message in the chat was edited or deleted for everyone. `_WS` suffix is load-bearing. */\nexport const MESSAGE_UPDATED_SUBSCRIPTION_WS = `\n subscription ChatMessageUpdated_WS($channelId: String!) {\n chatMessageUpdated(channelId: $channelId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** Who has received and read a message, for Message info: every member's watermarks and name. */\nexport const MESSAGE_RECEIPTS_QUERY = `\n query ChatMessageReceipts($id: String) {\n viewChannelDetail(id: $id) {\n id\n members { id user { id givenName familyName username email picture } lastViewedAt lastDeliveredAt }\n }\n }\n`;\n\n/** Marks the channel read for the caller; drives the unread badge to zero. */\nexport const VIEW_CHANNEL_MUTATION = `\n mutation ChatViewChannel($id: ID!) {\n viewChannel(id: $id)\n }\n`;\n\n/**\n * Silence (or, with `mutedUntil: null`, un-silence) the caller's notifications for one chat.\n * Stored on the caller's own member row, which is what the server's push fan-out checks, and\n * read back as `Channel.viewerMutedUntil` - private to the caller. \"Always\" is a far-future\n * instant (`chatModel.MUTE_FOREVER_ISO`).\n */\nexport const SET_CHANNEL_MUTE_MUTATION = `\n mutation ChatSetChannelMute($channelId: ID!, $mutedUntil: DateTime) {\n setChannelMute(channelId: $channelId, mutedUntil: $mutedUntil)\n }\n`;\n\n/**\n * Opens (or returns) the 1:1 conversation with `receiver`.\n *\n * Idempotent server-side: an existing DM for the pair comes back rather than a\n * duplicate, so the UI can call this every time someone taps a contact without\n * checking first.\n */\nexport const CREATE_DIRECT_CHANNEL_MUTATION = `\n mutation ChatCreateDirectChannel($receiver: [ID]!, $displayName: String!) {\n createDirectChannel(receiver: $receiver, displayName: $displayName) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/**\n * Live messages for one channel.\n *\n * `_WS` suffix is load-bearing - see the file header. `directTo` is left unset:\n * the server treats it as a second, user-scoped topic and passing the wrong one\n * silently delivers nothing.\n */\nexport const MESSAGE_ADDED_SUBSCRIPTION_WS = `\n subscription ChatMessageAdded_WS($channelId: String) {\n chatMessageAdded(channelId: $channelId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/**\n * Set the viewer's single Tapback on a message; `reaction: null` removes it.\n * Returns the message's reactions after the change.\n */\nexport const SET_MESSAGE_REACTION_MUTATION = `\n mutation ChatSetMessageReaction($channelId: String!, $postId: ID!, $reaction: String) {\n setMessageReaction(channelId: $channelId, postId: $postId, reaction: $reaction) {\n reaction\n count\n userIds\n }\n }\n`;\n\n/** Live reaction changes for one channel. `_WS` suffix is load-bearing - see the file header. */\nexport const REACTION_CHANGED_SUBSCRIPTION_WS = `\n subscription ChatReactionChanged_WS($channelId: String!) {\n chatReactionChanged(channelId: $channelId) {\n channelId\n postId\n reactions { reaction count userIds }\n }\n }\n`;\n\n/**\n * Which of these phone numbers already have an account.\n *\n * Sends SHA-256 hashes of E.164 numbers, never the numbers - the server never\n * receives a plaintext address book. Returns AnyObject: a list of\n * `{phoneHash, userId, username, displayName, picture}`.\n */\nexport const CONTACT_MATCHES_QUERY = `\n query ChatContactMatches($phoneHashes: [String!]!) {\n chatContactMatches(phoneHashes: $phoneHashes)\n }\n`;\n\nexport const YANTRA_CHAT_REGISTRATION_STATUS_QUERY = `\n query YantraChatRegistrationStatus {\n yantraChatRegistrationStatus {\n registered\n phoneE164\n memberId\n organizationId\n }\n }\n`;\n\nexport const REGISTER_YANTRA_CHAT_MEMBER_MUTATION = `\n mutation RegisterYantraChatMember($phoneE164: String!, $displayName: String) {\n registerYantraChatMember(phoneE164: $phoneE164, displayName: $displayName) {\n registered\n phoneE164\n memberId\n organizationId\n }\n }\n`;\n\nexport const MATCH_YANTRA_CHAT_MEMBERS_QUERY = `\n query MatchYantraChatMembersByPhone($phones: [String!]!, $viewerPhoneE164: String) {\n matchYantraChatMembersByPhone(phones: $phones, viewerPhoneE164: $viewerPhoneE164) {\n phoneE164\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\nexport const YANTRA_CHAT_MEMBERS_QUERY = `\n query YantraChatMembers {\n yantraChatMembers {\n phoneE164\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\n/**\n * The sender of a chat invite link, as the server knows them. The link's own `n=` name\n * is editable by anyone, so the conversation is opened with this name, and only when\n * the id belongs to a registered member (null otherwise).\n */\nexport const YANTRA_CHAT_INVITER_QUERY = `\n query YantraChatInviter($userId: String!) {\n yantraChatInviter(userId: $userId) {\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\n/** Composer draft persistence, server-side and per (member, channel). */\nexport const SET_DRAFT_MUTATION = `\n mutation ChatSetDraft($channelId: ID!, $text: String!) {\n setChannelDraft(channelId: $channelId, text: $text)\n }\n`;\n\nexport const DRAFT_QUERY = `\n query ChatDraft($channelId: ID!) {\n channelDraft(channelId: $channelId)\n }\n`;\n\n/** Every operation, for the contract test and for host-side preflight. */\nexport const CHAT_OPERATIONS: Readonly<Record<string, string>> = Object.freeze({\n CHANNELS_QUERY,\n CONTACT_MATCHES_QUERY,\n YANTRA_CHAT_REGISTRATION_STATUS_QUERY,\n REGISTER_YANTRA_CHAT_MEMBER_MUTATION,\n MATCH_YANTRA_CHAT_MEMBERS_QUERY,\n YANTRA_CHAT_MEMBERS_QUERY,\n YANTRA_CHAT_INVITER_QUERY,\n MESSAGES_QUERY,\n CHANNEL_READ_STATE_QUERY,\n MESSAGE_RECEIPTS_QUERY,\n SEND_MESSAGE_MUTATION,\n CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,\n ATTACH_UPLOADED_FILES_MUTATION,\n VIEW_CHANNEL_MUTATION,\n SET_CHANNEL_MUTE_MUTATION,\n MARK_CHANNEL_DELIVERED_MUTATION,\n SET_CHANNEL_LIST_STATE_MUTATION,\n EDIT_MESSAGE_MUTATION,\n DELETE_MESSAGE_FOR_EVERYONE_MUTATION,\n SET_MESSAGE_STARRED_MUTATION,\n STARRED_MESSAGES_QUERY,\n SEARCH_MESSAGES_QUERY,\n SEND_TYPING_MUTATION,\n TYPING_SUBSCRIPTION_WS,\n MESSAGE_UPDATED_SUBSCRIPTION_WS,\n CREATE_DIRECT_CHANNEL_MUTATION,\n MESSAGE_ADDED_SUBSCRIPTION_WS,\n SET_MESSAGE_REACTION_MUTATION,\n REACTION_CHANGED_SUBSCRIPTION_WS,\n SET_DRAFT_MUTATION,\n DRAFT_QUERY,\n});\n\n/**\n * The operation name inside a document string.\n *\n * Used by the contract test and by the host seam for logging. Intentionally a\n * regex rather than a parse: pulling in `graphql` to read a name would defeat\n * the whole point of shipping strings.\n */\nexport function operationNameOf(document: string): string | null {\n const match = /\\b(?:query|mutation|subscription)\\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(document);\n return match ? match[1] : null;\n}\n\nexport function operationTypeOf(document: string): 'query' | 'mutation' | 'subscription' | null {\n const match = /\\b(query|mutation|subscription)\\s+[A-Za-z_]/.exec(document);\n return match ? (match[1] as 'query' | 'mutation' | 'subscription') : null;\n}\n"],"names":[],"mappings":"AA0BO,MAAM,WAAc,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBpB,MAAM,cAAiB,GAAA;AAAA,IAAA,EACxB,WAAW;AAAA;AAAA;AAGjB,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAA,EAUH,WAAW,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAkBxB,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGhB,cAAc;AAAA;AAAA;AAAA;AAarB,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAA,EAKZ,cAAc;AAAA;AAAA;AAAA;AAAA;AAKzB,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGvB,cAAc;AAAA;AAAA;AAAA;AASrB,MAAM,oCAAuC,GAAA;AAAA;AAAA;AAAA;AAAA;AAO7C,MAAM,8BAAiC,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBvC,MAAM,wBAA2B,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAajC,MAAM,+BAAkC,GAAA;AAAA;AAAA;AAAA;AAAA;AAOxC,MAAM,+BAAkC,GAAA;AAAA;AAAA;AAAA;AAAA;AAOxC,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGvB,cAAc;AAAA;AAAA;AAAA;AAMrB,MAAM,oCAAuC,GAAA;AAAA;AAAA;AAAA,YAAA,EAGtC,cAAc;AAAA;AAAA;AAAA;AAMrB,MAAM,4BAA+B,GAAA;AAAA;AAAA;AAAA;AAAA;AAOrC,MAAM,sBAAyB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGxB,cAAc;AAAA;AAAA;AAAA;AAAA;AAOrB,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAA,EAKnB,WAAW;AAAA;AAAA;AAAA;AAAA;AAOtB,MAAM,oBAAuB,GAAA;AAAA;AAAA;AAAA;AAAA;AAO7B,MAAM,sBAAyB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY/B,MAAM,+BAAkC,GAAA;AAAA;AAAA;AAAA,YAAA,EAGjC,cAAc;AAAA;AAAA;AAAA;AAMrB,MAAM,sBAAyB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU/B,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA;AAAA;AAY9B,MAAM,yBAA4B,GAAA;AAAA;AAAA;AAAA;AAAA;AAalC,MAAM,8BAAiC,GAAA;AAAA;AAAA;AAAA,YAAA,EAGhC,cAAc;AAAA;AAAA;AAAA;AAYrB,MAAM,6BAAgC,GAAA;AAAA;AAAA;AAAA,YAAA,EAG/B,cAAc;AAAA;AAAA;AAAA;AASrB,MAAM,6BAAgC,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWtC,MAAM,gCAAmC,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBzC,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA;AAAA;AAK9B,MAAM,qCAAwC,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU9C,MAAM,oCAAuC,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoC7C,MAAM,yBAA4B,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWlC,MAAM,kBAAqB,GAAA;AAAA;AAAA;AAAA;AAAA"}
1
+ {"version":3,"file":"chatOperations.js","sources":["../../../src/features/messaging/chatOperations.ts"],"sourcesContent":["/**\n * Every GraphQL operation the chat remote sends, as plain strings.\n *\n * Strings, not `gql` documents, because the remote must not import\n * @apollo/client or graphql - the host parses and executes these on the app's\n * own client (see `ChatGraphQLSeam`). The upside beyond bundle size is that the\n * QUERY SHAPE ships over the air: a missing field or a wrong argument is a\n * surface deploy, not an App Store release.\n *\n * HARD REQUIREMENT - every operation is NAMED, and every subscription's name\n * ends in `_WS`. `portable-devices/mobile/src/config/base-apollo-client.ts`\n * routes ws-vs-http with `operationName.endsWith('_WS')`; an anonymous\n * operation makes that `undefined.endsWith(...)`, which throws for EVERY\n * operation the app sends, not just this one. `chatOperations.test.ts` asserts\n * both properties over every export in this file, so a new operation cannot\n * quietly break the app's transport.\n */\n\n/**\n * Shared field set for a message.\n *\n * `author` and `creator` are both selected on purpose: messenger-box populates\n * `author` on some write paths and `creator` on others (`sendMessage` sets\n * creator), and a transcript that reads only one shows \"Unknown\" for half the\n * messages. `chatModel.pickAuthor` collapses them.\n */\nexport const POST_FIELDS = `\n id\n message\n type\n createdAt\n updatedAt\n editedAt\n deletedAt\n rootId\n parentId\n filenames\n fromServer\n props\n author { id givenName familyName username email picture }\n creator { id givenName familyName username email picture }\n reactionSummary { reaction count userIds }\n viewerStarred\n`;\n\n/**\n * A message plus its attached files, for the transcript (not the chat list, where resolving\n * files per row would cost a lookup per conversation - its preview uses `props.attachmentKind`).\n */\nexport const MESSAGE_FIELDS = `\n ${POST_FIELDS}\n files { data { id name mimeType url width height size } }\n`;\n\nconst CHANNEL_FIELDS = `\n id\n type\n orgName\n title\n displayName\n topic\n description\n lastPostAt\n totalMsgCount\n createdAt\n settings\n lastMessage { ${POST_FIELDS} }\n creator { id givenName familyName username email picture }\n members { id roles user { id givenName familyName username email picture } msgCount lastViewedAt lastDeliveredAt }\n viewerMutedUntil\n viewerPinnedAt\n viewerArchivedAt\n viewerDraft\n`;\n// NOTE: Channel.viewerUnreadMentions (the group \"@\" unread badge) is intentionally NOT selected\n// here yet. It only exists once the wave-4 server (messenger-box #886) is deployed, and selecting\n// a field the backend lacks fails the ENTIRE list query with a GraphQL 400 - which hard-breaks the\n// whole Chats screen (\"Could not load messages for this chat.\"). The client defaults the count to\n// 0 (chatModel.toConversation: `channel.viewerUnreadMentions ?? 0`), so the badge is simply absent\n// until the backend has the field. Re-add `viewerUnreadMentions` to CHANNEL_FIELDS in the same\n// change that deploys the backend. Deploy order is always backend -> chat surface.\n\n/**\n * The conversation list.\n *\n * `criteria.members.$elemMatch.user` is NOT sent: the server derives membership\n * from the session (`accountId`) and a client-supplied member filter is exactly\n * the kind of thing that lets a caller read someone else's list. Org scoping is\n * likewise server-side now (`channelOrgScope`). Native Messages passes\n * `criteria: { type: DIRECT, orgName: yantra-chat }` via `nativeChatChannelQuery`.\n */\nexport const CHANNELS_QUERY = `\n query ChatChannels($limit: Int, $skip: Int, $criteria: AnyObject, $sort: Sort) {\n channelsByUser(limit: $limit, skip: $skip, criteria: $criteria, sort: $sort) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/**\n * One page of a transcript, newest first.\n *\n * Paged with `skip`, which is what the server exposes. That is offset paging and\n * it can repeat or drop a row if messages arrive mid-scroll; `chatModel.mergePosts`\n * de-duplicates by id so the visible effect is bounded. A cursor (`before: id`)\n * would be strictly better and is worth adding server-side later.\n */\nexport const MESSAGES_QUERY = `\n query ChatMessages($channelId: ID, $limit: Int! = 30, $skip: Int! = 0, $sort: Sort) {\n messages(channelId: $channelId, limit: $limit, skip: $skip, sort: $sort) {\n totalCount\n data {\n ${MESSAGE_FIELDS}\n }\n }\n }\n`;\n\nexport const SEND_MESSAGE_MUTATION = `\n mutation ChatSendMessage($channelId: String!, $messageInput: MessageInput!, $postId: ID) {\n sendMessage(channelId: $channelId, messageInput: $messageInput, postId: $postId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/**\n * Attachments, step 1: one presigned S3 PUT URL per file for a post that does not exist yet\n * (the client picks the post id; `sendMessage` then creates the post with that id).\n */\nexport const CREATE_MESSAGE_UPLOAD_LINKS_MUTATION = `\n mutation ChatCreateMessageUploadLinks($postId: ID!, $filenames: [String!]!) {\n createMessageFilesUploadLink(postId: $postId, filenames: $filenames)\n }\n`;\n\n/** Attachments, step 3 (after the PUTs): record the uploaded files against the post id. */\nexport const ATTACH_UPLOADED_FILES_MUTATION = `\n mutation ChatAttachUploadedFiles($postId: ID!, $files: [UploadedFileInput!]!) {\n attachUploadedFilesToMessage(postId: $postId, files: $files) {\n id\n name\n mimeType\n url\n width\n height\n }\n }\n`;\n\n/**\n * Who has read how far in one chat - just the members' `lastViewedAt`. Polled while\n * the viewer's newest message is still unread, so its ticks turn blue without a reload.\n */\nexport const CHANNEL_READ_STATE_QUERY = `\n query ChatChannelReadState($id: String) {\n viewChannelDetail(id: $id) {\n id\n members { id user { id } lastViewedAt lastDeliveredAt }\n }\n }\n`;\n\n/**\n * This phone has the channel's messages up to now - the senders' two gray ticks. Called when a\n * message arrives (live or with the list), never more than once per chat every few seconds.\n */\nexport const MARK_CHANNEL_DELIVERED_MUTATION = `\n mutation ChatMarkChannelDelivered($channelId: ID!) {\n markChannelDelivered(channelId: $channelId)\n }\n`;\n\n/** Pin to the top of, or archive out of, the viewer's own chat list. An omitted flag is left as is. */\nexport const SET_CHANNEL_LIST_STATE_MUTATION = `\n mutation ChatSetChannelListState($channelId: ID!, $pinned: Boolean, $archived: Boolean) {\n setChannelListState(channelId: $channelId, pinned: $pinned, archived: $archived)\n }\n`;\n\n/** Rewrite the viewer's own message; everyone else sees it through `chatMessageUpdated`. */\nexport const EDIT_MESSAGE_MUTATION = `\n mutation ChatEditMessage($messageId: MessageIdentifier!, $messageInput: MessageInput!) {\n editMessage(messageId: $messageId, messageInput: $messageInput) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** WhatsApp's \"Delete for everyone\" (own messages, within 48 hours): leaves a tombstone. */\nexport const DELETE_MESSAGE_FOR_EVERYONE_MUTATION = `\n mutation ChatDeleteMessageForEveryone($messageId: MessageIdentifier!) {\n deleteMessageForEveryone(messageId: $messageId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** Star or unstar a message for the viewer only. */\nexport const SET_MESSAGE_STARRED_MUTATION = `\n mutation ChatSetMessageStarred($messageId: MessageIdentifier!, $starred: Boolean!) {\n setMessageStarred(messageId: $messageId, starred: $starred)\n }\n`;\n\n/**\n * Keep (or unkeep) a disappearing message so the auto-delete sweep leaves it. Returns the\n * message with its updated props (props.kept). NOTE: we deliberately do NOT select Post.expiresAt\n * anywhere - the client never needs the raw timestamp (disappearing state is read from\n * channel.settings.disappearingSeconds and removal arrives as props.disappeared), and selecting a\n * brand-new scalar on Post would 400 the whole list/thread against a backend that lacks it (the\n * wave-4 viewerUnreadMentions lesson). props is AnyObject, so props.kept is always safe.\n */\nexport const KEEP_MESSAGE_MUTATION = `\n mutation ChatKeepMessage($messageId: MessageIdentifier!, $keep: Boolean!) {\n keepMessage(messageId: $messageId, keep: $keep) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** The viewer's starred messages, newest first, with the chat each one is in. */\nexport const STARRED_MESSAGES_QUERY = `\n query ChatStarredMessages($limit: Int, $skip: Int) {\n starredMessages(limit: $limit, skip: $skip) {\n ${MESSAGE_FIELDS}\n channel { id }\n }\n }\n`;\n\n/** Search one chat's messages (newest first). The server checks membership. */\nexport const SEARCH_MESSAGES_QUERY = `\n query ChatSearchMessages($query: String!, $channelId: ID, $limit: Int, $skip: Int) {\n searchMessages(query: $query, channelId: $channelId, limit: $limit, skip: $skip) {\n totalCount\n data {\n ${POST_FIELDS}\n }\n }\n }\n`;\n\n/**\n * The \"Media, links and docs\" gallery. `kind` is 'media' | 'docs' | 'links'. A given channelId\n * scopes to that one chat; omitting it searches every chat the viewer is in (the \"all chats\" view).\n * Returns full messages so Post.files renders attachments and `message` carries link text.\n */\nexport const MEDIA_MESSAGES_QUERY = `\n query ChatMediaMessages($channelId: ID, $kind: String!, $limit: Int, $skip: Int) {\n mediaMessages(channelId: $channelId, kind: $kind, limit: $limit, skip: $skip) {\n totalCount\n data {\n ${MESSAGE_FIELDS}\n channel { id }\n }\n }\n }\n`;\n\n/** \"typing…\": start while composing, stop on send or after a pause. */\nexport const SEND_TYPING_MUTATION = `\n mutation ChatSendTyping($channelId: String!, $action: String!) {\n sendTypingIndicator(channelId: $channelId, action: $action)\n }\n`;\n\n/** Who is typing in a chat. `_WS` suffix is load-bearing - see the file header. */\nexport const TYPING_SUBSCRIPTION_WS = `\n subscription ChatTyping_WS($channelId: String!) {\n typingIndicator(channelId: $channelId) {\n userId\n displayName\n channelId\n action\n }\n }\n`;\n\n/** A message in the chat was edited or deleted for everyone. `_WS` suffix is load-bearing. */\nexport const MESSAGE_UPDATED_SUBSCRIPTION_WS = `\n subscription ChatMessageUpdated_WS($channelId: String!) {\n chatMessageUpdated(channelId: $channelId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** Who has received and read a message, for Message info: every member's watermarks and name. */\nexport const MESSAGE_RECEIPTS_QUERY = `\n query ChatMessageReceipts($id: String) {\n viewChannelDetail(id: $id) {\n id\n members { id user { id givenName familyName username email picture } lastViewedAt lastDeliveredAt }\n }\n }\n`;\n\n/** Marks the channel read for the caller; drives the unread badge to zero. */\nexport const VIEW_CHANNEL_MUTATION = `\n mutation ChatViewChannel($id: ID!) {\n viewChannel(id: $id)\n }\n`;\n\n/**\n * Silence (or, with `mutedUntil: null`, un-silence) the caller's notifications for one chat.\n * Stored on the caller's own member row, which is what the server's push fan-out checks, and\n * read back as `Channel.viewerMutedUntil` - private to the caller. \"Always\" is a far-future\n * instant (`chatModel.MUTE_FOREVER_ISO`).\n */\nexport const SET_CHANNEL_MUTE_MUTATION = `\n mutation ChatSetChannelMute($channelId: ID!, $mutedUntil: DateTime) {\n setChannelMute(channelId: $channelId, mutedUntil: $mutedUntil)\n }\n`;\n\n/**\n * Opens (or returns) the 1:1 conversation with `receiver`.\n *\n * Idempotent server-side: an existing DM for the pair comes back rather than a\n * duplicate, so the UI can call this every time someone taps a contact without\n * checking first.\n */\nexport const CREATE_DIRECT_CHANNEL_MUTATION = `\n mutation ChatCreateDirectChannel($receiver: [ID]!, $displayName: String!) {\n createDirectChannel(receiver: $receiver, displayName: $displayName) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/** WhatsApp-style group management. Each returns the updated channel so the list and info refresh. */\nexport const CREATE_GROUP_CHANNEL_MUTATION = `\n mutation ChatCreateGroup($name: String!, $memberIds: [ID!]!, $description: String) {\n createGroupChannel(name: $name, memberIds: $memberIds, description: $description) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\nexport const ADD_GROUP_MEMBERS_MUTATION = `\n mutation ChatAddGroupMembers($channelId: String!, $memberIds: [ID!]!) {\n addGroupMembers(channelId: $channelId, memberIds: $memberIds) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\nexport const REMOVE_GROUP_MEMBER_MUTATION = `\n mutation ChatRemoveGroupMember($channelId: String!, $memberId: ID!) {\n removeGroupMember(channelId: $channelId, memberId: $memberId) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\nexport const LEAVE_GROUP_MUTATION = `\n mutation ChatLeaveGroup($channelId: String!) {\n leaveGroup(channelId: $channelId)\n }\n`;\n\nexport const SET_GROUP_ADMIN_MUTATION = `\n mutation ChatSetGroupAdmin($channelId: String!, $memberId: ID!, $admin: Boolean!) {\n setGroupAdmin(channelId: $channelId, memberId: $memberId, admin: $admin) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\nexport const UPDATE_GROUP_INFO_MUTATION = `\n mutation ChatUpdateGroupInfo($channelId: String!, $name: String, $description: String) {\n updateGroupInfo(channelId: $channelId, name: $name, description: $description) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\nexport const SET_GROUP_SEND_POLICY_MUTATION = `\n mutation ChatSetGroupSendPolicy($channelId: String!, $onlyAdmins: Boolean!) {\n setGroupSendPolicy(channelId: $channelId, onlyAdmins: $onlyAdmins) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/**\n * Turn disappearing messages on (seconds = 86400 / 604800 / 7776000) or off (0) for a chat.\n * Returns the channel; the new timer is read back from `settings.disappearingSeconds` (settings is\n * AnyObject, so no new-scalar 400 risk). Groups: admins only. DMs: either member (server-enforced).\n */\nexport const SET_DISAPPEARING_MUTATION = `\n mutation ChatSetDisappearing($channelId: String!, $seconds: Int!) {\n setDisappearing(channelId: $channelId, seconds: $seconds) {\n ${CHANNEL_FIELDS}\n }\n }\n`;\n\n/**\n * Live messages for one channel.\n *\n * `_WS` suffix is load-bearing - see the file header. `directTo` is left unset:\n * the server treats it as a second, user-scoped topic and passing the wrong one\n * silently delivers nothing.\n */\nexport const MESSAGE_ADDED_SUBSCRIPTION_WS = `\n subscription ChatMessageAdded_WS($channelId: String) {\n chatMessageAdded(channelId: $channelId) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/**\n * Set the viewer's single Tapback on a message; `reaction: null` removes it.\n * Returns the message's reactions after the change.\n */\nexport const SET_MESSAGE_REACTION_MUTATION = `\n mutation ChatSetMessageReaction($channelId: String!, $postId: ID!, $reaction: String) {\n setMessageReaction(channelId: $channelId, postId: $postId, reaction: $reaction) {\n reaction\n count\n userIds\n }\n }\n`;\n\n/**\n * Create a poll (its own mutation, not sendMessage, which strips props.poll). The server assigns\n * option ids; the whole poll rides props.poll and comes back on the returned message like any send.\n */\nexport const CREATE_POLL_MUTATION = `\n mutation ChatCreatePoll($channelId: String!, $question: String!, $options: [String!]!, $multiple: Boolean) {\n createPoll(channelId: $channelId, question: $question, options: $options, multiple: $multiple) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/**\n * Cast/change the viewer's poll vote. `optionIds` is the viewer's COMPLETE selection after the tap\n * (empty clears it); single-choice polls keep at most one. Returns the poll message with new tallies;\n * everyone else gets the same via chatMessageUpdated.\n */\nexport const VOTE_POLL_MUTATION = `\n mutation ChatVotePoll($channelId: String!, $postId: ID!, $optionIds: [String!]!) {\n votePoll(channelId: $channelId, postId: $postId, optionIds: $optionIds) {\n ${MESSAGE_FIELDS}\n }\n }\n`;\n\n/** Live reaction changes for one channel. `_WS` suffix is load-bearing - see the file header. */\nexport const REACTION_CHANGED_SUBSCRIPTION_WS = `\n subscription ChatReactionChanged_WS($channelId: String!) {\n chatReactionChanged(channelId: $channelId) {\n channelId\n postId\n reactions { reaction count userIds }\n }\n }\n`;\n\n/**\n * Which of these phone numbers already have an account.\n *\n * Sends SHA-256 hashes of E.164 numbers, never the numbers - the server never\n * receives a plaintext address book. Returns AnyObject: a list of\n * `{phoneHash, userId, username, displayName, picture}`.\n */\nexport const CONTACT_MATCHES_QUERY = `\n query ChatContactMatches($phoneHashes: [String!]!) {\n chatContactMatches(phoneHashes: $phoneHashes)\n }\n`;\n\n/**\n * Block / unblock / report, WhatsApp-style. Block is one-directional and silent (the blocked peer\n * is never told). `blockedContacts` returns AnyObject: a list of\n * `{userId, username, displayName, picture, blockedAt}`. `reportTarget`'s targetType is\n * CONTACT | GROUP | MESSAGE; alsoBlock implements \"Report and block\" in one round-trip.\n */\nexport const BLOCK_CONTACT_MUTATION = `\n mutation ChatBlockContact($userId: ID!) {\n blockContact(userId: $userId)\n }\n`;\n\nexport const UNBLOCK_CONTACT_MUTATION = `\n mutation ChatUnblockContact($userId: ID!) {\n unblockContact(userId: $userId)\n }\n`;\n\nexport const BLOCKED_CONTACTS_QUERY = `\n query ChatBlockedContacts {\n blockedContacts\n }\n`;\n\nexport const REPORT_TARGET_MUTATION = `\n mutation ChatReportTarget($targetType: String!, $targetId: ID!, $reason: String, $alsoBlock: Boolean) {\n reportTarget(targetType: $targetType, targetId: $targetId, reason: $reason, alsoBlock: $alsoBlock)\n }\n`;\n\nexport const YANTRA_CHAT_REGISTRATION_STATUS_QUERY = `\n query YantraChatRegistrationStatus {\n yantraChatRegistrationStatus {\n registered\n phoneE164\n memberId\n organizationId\n }\n }\n`;\n\nexport const REGISTER_YANTRA_CHAT_MEMBER_MUTATION = `\n mutation RegisterYantraChatMember($phoneE164: String!, $displayName: String) {\n registerYantraChatMember(phoneE164: $phoneE164, displayName: $displayName) {\n registered\n phoneE164\n memberId\n organizationId\n }\n }\n`;\n\nexport const MATCH_YANTRA_CHAT_MEMBERS_QUERY = `\n query MatchYantraChatMembersByPhone($phones: [String!]!, $viewerPhoneE164: String) {\n matchYantraChatMembersByPhone(phones: $phones, viewerPhoneE164: $viewerPhoneE164) {\n phoneE164\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\nexport const YANTRA_CHAT_MEMBERS_QUERY = `\n query YantraChatMembers {\n yantraChatMembers {\n phoneE164\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\n/**\n * The sender of a chat invite link, as the server knows them. The link's own `n=` name\n * is editable by anyone, so the conversation is opened with this name, and only when\n * the id belongs to a registered member (null otherwise).\n */\nexport const YANTRA_CHAT_INVITER_QUERY = `\n query YantraChatInviter($userId: String!) {\n yantraChatInviter(userId: $userId) {\n userId\n displayName\n avatarUrl\n }\n }\n`;\n\n/** Composer draft persistence, server-side and per (member, channel). */\nexport const SET_DRAFT_MUTATION = `\n mutation ChatSetDraft($channelId: ID!, $text: String!) {\n setChannelDraft(channelId: $channelId, text: $text)\n }\n`;\n\nexport const DRAFT_QUERY = `\n query ChatDraft($channelId: ID!) {\n channelDraft(channelId: $channelId)\n }\n`;\n\n/** Every operation, for the contract test and for host-side preflight. */\nexport const CHAT_OPERATIONS: Readonly<Record<string, string>> = Object.freeze({\n CHANNELS_QUERY,\n CONTACT_MATCHES_QUERY,\n YANTRA_CHAT_REGISTRATION_STATUS_QUERY,\n REGISTER_YANTRA_CHAT_MEMBER_MUTATION,\n MATCH_YANTRA_CHAT_MEMBERS_QUERY,\n YANTRA_CHAT_MEMBERS_QUERY,\n YANTRA_CHAT_INVITER_QUERY,\n MESSAGES_QUERY,\n CHANNEL_READ_STATE_QUERY,\n MESSAGE_RECEIPTS_QUERY,\n SEND_MESSAGE_MUTATION,\n CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,\n ATTACH_UPLOADED_FILES_MUTATION,\n VIEW_CHANNEL_MUTATION,\n SET_CHANNEL_MUTE_MUTATION,\n MARK_CHANNEL_DELIVERED_MUTATION,\n SET_CHANNEL_LIST_STATE_MUTATION,\n EDIT_MESSAGE_MUTATION,\n DELETE_MESSAGE_FOR_EVERYONE_MUTATION,\n SET_MESSAGE_STARRED_MUTATION,\n KEEP_MESSAGE_MUTATION,\n STARRED_MESSAGES_QUERY,\n SEARCH_MESSAGES_QUERY,\n MEDIA_MESSAGES_QUERY,\n SEND_TYPING_MUTATION,\n TYPING_SUBSCRIPTION_WS,\n MESSAGE_UPDATED_SUBSCRIPTION_WS,\n CREATE_DIRECT_CHANNEL_MUTATION,\n CREATE_GROUP_CHANNEL_MUTATION,\n ADD_GROUP_MEMBERS_MUTATION,\n REMOVE_GROUP_MEMBER_MUTATION,\n LEAVE_GROUP_MUTATION,\n SET_GROUP_ADMIN_MUTATION,\n UPDATE_GROUP_INFO_MUTATION,\n SET_GROUP_SEND_POLICY_MUTATION,\n SET_DISAPPEARING_MUTATION,\n MESSAGE_ADDED_SUBSCRIPTION_WS,\n SET_MESSAGE_REACTION_MUTATION,\n CREATE_POLL_MUTATION,\n VOTE_POLL_MUTATION,\n REACTION_CHANGED_SUBSCRIPTION_WS,\n BLOCK_CONTACT_MUTATION,\n UNBLOCK_CONTACT_MUTATION,\n BLOCKED_CONTACTS_QUERY,\n REPORT_TARGET_MUTATION,\n SET_DRAFT_MUTATION,\n DRAFT_QUERY,\n});\n\n/**\n * The operation name inside a document string.\n *\n * Used by the contract test and by the host seam for logging. Intentionally a\n * regex rather than a parse: pulling in `graphql` to read a name would defeat\n * the whole point of shipping strings.\n */\nexport function operationNameOf(document: string): string | null {\n const match = /\\b(?:query|mutation|subscription)\\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(document);\n return match ? match[1] : null;\n}\n\nexport function operationTypeOf(document: string): 'query' | 'mutation' | 'subscription' | null {\n const match = /\\b(query|mutation|subscription)\\s+[A-Za-z_]/.exec(document);\n return match ? (match[1] as 'query' | 'mutation' | 'subscription') : null;\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,10 @@
1
1
  import {SortEnum,RoomType}from'common';import {YANTRA_CHAT_ORG_MONGO_ID,isYantraChatOrgKey}from'./yantraChatOrg.js';const NATIVE_CHAT_LIST_PAGE_SIZE = 20;
2
2
  const NATIVE_CHAT_CHANNELS_QUERY_VARS = {
3
3
  criteria: {
4
- type: RoomType.Direct,
4
+ // One-to-one chats and WhatsApp-style groups both live in the chat org.
5
+ type: {
6
+ $in: [RoomType.Direct, RoomType.Group]
7
+ },
5
8
  organization: YANTRA_CHAT_ORG_MONGO_ID
6
9
  },
7
10
  limit: NATIVE_CHAT_LIST_PAGE_SIZE,
@@ -21,7 +24,9 @@ function isNativeMessengerChannel(channel) {
21
24
  if (!channel.id) return false;
22
25
  const t = String((_a = channel.type) != null ? _a : "").toUpperCase();
23
26
  if (t === RoomType.Aiassistant || t === "AIASSISTANT") return false;
24
- if (t !== RoomType.Direct && t !== "DIRECT") return false;
27
+ const isDirect = t === RoomType.Direct || t === "DIRECT";
28
+ const isGroup = t === RoomType.Group || t === "GROUP";
29
+ if (!isDirect && !isGroup) return false;
25
30
  if (channel.orgName && !isYantraChatOrgKey(channel.orgName)) return false;
26
31
  return true;
27
32
  }export{NATIVE_CHAT_CHANNELS_QUERY_VARS,NATIVE_CHAT_LIST_PAGE_SIZE,isMongoAccountId,isNativeMessengerChannel};//# sourceMappingURL=nativeChatChannelQuery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeChatChannelQuery.js","sources":["../../../src/features/messaging/nativeChatChannelQuery.ts"],"sourcesContent":["import { RoomType, SortEnum } from 'common';\nimport type { WireChannel } from './chatModel';\nimport { isYantraChatOrgKey, YANTRA_CHAT_ORG_MONGO_ID } from './yantraChatOrg';\n\n/**\n * Variables for the native Messages list (`ChatChannels` / `channelsByUser`).\n *\n * messenger-box's createDirectChannel stores DMs with `organization` = the chat\n * org ObjectId and NO `orgName`, so filtering on orgName matches nothing. Do not\n * switch Apollo org context for this query — Home and other apps stay on the default org.\n */\n/** Conversations fetched per page on the Messages list. */\nexport const NATIVE_CHAT_LIST_PAGE_SIZE = 20;\n\n/** First page; later pages reuse these vars with a larger `skip`. */\nexport const NATIVE_CHAT_CHANNELS_QUERY_VARS = {\n criteria: {\n type: RoomType.Direct,\n organization: YANTRA_CHAT_ORG_MONGO_ID,\n },\n limit: NATIVE_CHAT_LIST_PAGE_SIZE,\n skip: 0,\n sort: { key: 'updatedAt' as const, value: SortEnum.Desc },\n};\n\nconst ACCOUNT_OBJECT_ID = /^[a-f0-9]{24}$/i;\n\nexport function isMongoAccountId(value: string | null | undefined): boolean {\n return Boolean(value && ACCOUNT_OBJECT_ID.test(value));\n}\n\n/** Defense-in-depth filter on list rows returned from the server. */\nexport function isNativeMessengerChannel(\n channel: Partial<Pick<WireChannel, 'id' | 'type' | 'orgName'>> | null | undefined,\n): boolean {\n if (!channel) return false;\n if (!channel.id) return false;\n const t = String(channel.type ?? '').toUpperCase();\n if (t === RoomType.Aiassistant || t === 'AIASSISTANT') return false;\n if (t !== RoomType.Direct && t !== 'DIRECT') return false;\n if (channel.orgName && !isYantraChatOrgKey(channel.orgName)) return false;\n return true;\n}\n"],"names":[],"mappings":"oHAYO,MAAM,0BAA6B,GAAA;AAGnC,MAAM,+BAAkC,GAAA;AAAA,EAC7C,QAAU,EAAA;AAAA,IACR,MAAM,QAAS,CAAA,MAAA;AAAA,IACf,YAAc,EAAA;AAAA,GAChB;AAAA,EACA,KAAO,EAAA,0BAAA;AAAA,EACP,IAAM,EAAA,CAAA;AAAA,EACN,IAAM,EAAA;AAAA,IACJ,GAAK,EAAA,WAAA;AAAA,IACL,OAAO,QAAS,CAAA;AAAA;AAEpB;AACA,MAAM,iBAAoB,GAAA,iBAAA;AACnB,SAAS,iBAAiB,KAA2C,EAAA;AAC1E,EAAA,OAAO,OAAQ,CAAA,KAAA,IAAS,iBAAkB,CAAA,IAAA,CAAK,KAAK,CAAC,CAAA;AACvD;AAGO,SAAS,yBAAyB,OAA4F,EAAA;AAjCrI,EAAA,IAAA,EAAA;AAkCE,EAAI,IAAA,CAAC,SAAgB,OAAA,KAAA;AACrB,EAAI,IAAA,CAAC,OAAQ,CAAA,EAAA,EAAW,OAAA,KAAA;AACxB,EAAA,MAAM,IAAI,MAAO,CAAA,CAAA,EAAA,GAAA,OAAA,CAAQ,SAAR,IAAgB,GAAA,EAAA,GAAA,EAAE,EAAE,WAAY,EAAA;AACjD,EAAA,IAAI,CAAM,KAAA,QAAA,CAAS,WAAe,IAAA,CAAA,KAAM,eAAsB,OAAA,KAAA;AAC9D,EAAA,IAAI,CAAM,KAAA,QAAA,CAAS,MAAU,IAAA,CAAA,KAAM,UAAiB,OAAA,KAAA;AACpD,EAAA,IAAI,QAAQ,OAAW,IAAA,CAAC,mBAAmB,OAAQ,CAAA,OAAO,GAAU,OAAA,KAAA;AACpE,EAAO,OAAA,IAAA;AACT"}
1
+ {"version":3,"file":"nativeChatChannelQuery.js","sources":["../../../src/features/messaging/nativeChatChannelQuery.ts"],"sourcesContent":["import { RoomType, SortEnum } from 'common';\nimport type { WireChannel } from './chatModel';\nimport { isYantraChatOrgKey, YANTRA_CHAT_ORG_MONGO_ID } from './yantraChatOrg';\n\n/**\n * Variables for the native Messages list (`ChatChannels` / `channelsByUser`).\n *\n * messenger-box's createDirectChannel stores DMs with `organization` = the chat\n * org ObjectId and NO `orgName`, so filtering on orgName matches nothing. Do not\n * switch Apollo org context for this query — Home and other apps stay on the default org.\n */\n/** Conversations fetched per page on the Messages list. */\nexport const NATIVE_CHAT_LIST_PAGE_SIZE = 20;\n\n/** First page; later pages reuse these vars with a larger `skip`. */\nexport const NATIVE_CHAT_CHANNELS_QUERY_VARS = {\n criteria: {\n // One-to-one chats and WhatsApp-style groups both live in the chat org.\n type: { $in: [RoomType.Direct, RoomType.Group] },\n organization: YANTRA_CHAT_ORG_MONGO_ID,\n },\n limit: NATIVE_CHAT_LIST_PAGE_SIZE,\n skip: 0,\n sort: { key: 'updatedAt' as const, value: SortEnum.Desc },\n};\n\nconst ACCOUNT_OBJECT_ID = /^[a-f0-9]{24}$/i;\n\nexport function isMongoAccountId(value: string | null | undefined): boolean {\n return Boolean(value && ACCOUNT_OBJECT_ID.test(value));\n}\n\n/** Defense-in-depth filter on list rows returned from the server. */\nexport function isNativeMessengerChannel(\n channel: Partial<Pick<WireChannel, 'id' | 'type' | 'orgName'>> | null | undefined,\n): boolean {\n if (!channel) return false;\n if (!channel.id) return false;\n const t = String(channel.type ?? '').toUpperCase();\n if (t === RoomType.Aiassistant || t === 'AIASSISTANT') return false;\n // Person-to-person chat is one-to-one (DIRECT) or a group (GROUP); everything else\n // (service, public, AI assistant) is not shown in the Messages list.\n const isDirect = t === RoomType.Direct || t === 'DIRECT';\n const isGroup = t === RoomType.Group || t === 'GROUP';\n if (!isDirect && !isGroup) return false;\n if (channel.orgName && !isYantraChatOrgKey(channel.orgName)) return false;\n return true;\n}\n"],"names":[],"mappings":"oHAYO,MAAM,0BAA6B,GAAA;AAGnC,MAAM,+BAAkC,GAAA;AAAA,EAC7C,QAAU,EAAA;AAAA;AAAA,IAER,IAAM,EAAA;AAAA,MACJ,GAAK,EAAA,CAAC,QAAS,CAAA,MAAA,EAAQ,SAAS,KAAK;AAAA,KACvC;AAAA,IACA,YAAc,EAAA;AAAA,GAChB;AAAA,EACA,KAAO,EAAA,0BAAA;AAAA,EACP,IAAM,EAAA,CAAA;AAAA,EACN,IAAM,EAAA;AAAA,IACJ,GAAK,EAAA,WAAA;AAAA,IACL,OAAO,QAAS,CAAA;AAAA;AAEpB;AACA,MAAM,iBAAoB,GAAA,iBAAA;AACnB,SAAS,iBAAiB,KAA2C,EAAA;AAC1E,EAAA,OAAO,OAAQ,CAAA,KAAA,IAAS,iBAAkB,CAAA,IAAA,CAAK,KAAK,CAAC,CAAA;AACvD;AAGO,SAAS,yBAAyB,OAA4F,EAAA;AApCrI,EAAA,IAAA,EAAA;AAqCE,EAAI,IAAA,CAAC,SAAgB,OAAA,KAAA;AACrB,EAAI,IAAA,CAAC,OAAQ,CAAA,EAAA,EAAW,OAAA,KAAA;AACxB,EAAA,MAAM,IAAI,MAAO,CAAA,CAAA,EAAA,GAAA,OAAA,CAAQ,SAAR,IAAgB,GAAA,EAAA,GAAA,EAAE,EAAE,WAAY,EAAA;AACjD,EAAA,IAAI,CAAM,KAAA,QAAA,CAAS,WAAe,IAAA,CAAA,KAAM,eAAsB,OAAA,KAAA;AAG9D,EAAA,MAAM,QAAW,GAAA,CAAA,KAAM,QAAS,CAAA,MAAA,IAAU,CAAM,KAAA,QAAA;AAChD,EAAA,MAAM,OAAU,GAAA,CAAA,KAAM,QAAS,CAAA,KAAA,IAAS,CAAM,KAAA,OAAA;AAC9C,EAAA,IAAI,CAAC,QAAA,IAAY,CAAC,OAAA,EAAgB,OAAA,KAAA;AAClC,EAAA,IAAI,QAAQ,OAAW,IAAA,CAAC,mBAAmB,OAAQ,CAAA,OAAO,GAAU,OAAA,KAAA;AACpE,EAAO,OAAA,IAAA;AACT"}
@@ -1,4 +1,4 @@
1
- import*as React from'react';import {CHANNELS_QUERY,SET_CHANNEL_LIST_STATE_MUTATION,MARK_CHANNEL_DELIVERED_MUTATION,MESSAGE_ADDED_SUBSCRIPTION_WS,CREATE_DIRECT_CHANNEL_MUTATION,MESSAGES_QUERY,SEND_MESSAGE_MUTATION,CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,ATTACH_UPLOADED_FILES_MUTATION,REACTION_CHANGED_SUBSCRIPTION_WS,SET_MESSAGE_REACTION_MUTATION,MESSAGE_UPDATED_SUBSCRIPTION_WS,EDIT_MESSAGE_MUTATION,DELETE_MESSAGE_FOR_EVERYONE_MUTATION,SET_MESSAGE_STARRED_MUTATION,TYPING_SUBSCRIPTION_WS,SEND_TYPING_MUTATION,VIEW_CHANNEL_MUTATION,CHANNEL_READ_STATE_QUERY}from'./chatOperations.js';import {mergeConversations,toConversation,sortConversations,patchConversationAfterUpdate,patchConversationAfterPost,mergeMessages,toChatMessage,reconcileOptimistic,ATTACHMENT_LABELS,applyReactionSummary,withViewerReaction,readWatermarkOf,deliveredWatermarkOf}from'./chatModel.js';import {NATIVE_CHAT_CHANNELS_QUERY_VARS,isNativeMessengerChannel,NATIVE_CHAT_LIST_PAGE_SIZE,isMongoAccountId}from'./nativeChatChannelQuery.js';var __defProp = Object.defineProperty;
1
+ import*as React from'react';import {CHANNELS_QUERY,SET_CHANNEL_LIST_STATE_MUTATION,MARK_CHANNEL_DELIVERED_MUTATION,MESSAGE_ADDED_SUBSCRIPTION_WS,CREATE_DIRECT_CHANNEL_MUTATION,CREATE_GROUP_CHANNEL_MUTATION,ADD_GROUP_MEMBERS_MUTATION,REMOVE_GROUP_MEMBER_MUTATION,SET_GROUP_ADMIN_MUTATION,UPDATE_GROUP_INFO_MUTATION,SET_GROUP_SEND_POLICY_MUTATION,LEAVE_GROUP_MUTATION,SET_DISAPPEARING_MUTATION,BLOCKED_CONTACTS_QUERY,BLOCK_CONTACT_MUTATION,UNBLOCK_CONTACT_MUTATION,REPORT_TARGET_MUTATION,MESSAGES_QUERY,SEND_MESSAGE_MUTATION,CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,ATTACH_UPLOADED_FILES_MUTATION,REACTION_CHANGED_SUBSCRIPTION_WS,SET_MESSAGE_REACTION_MUTATION,MESSAGE_UPDATED_SUBSCRIPTION_WS,EDIT_MESSAGE_MUTATION,DELETE_MESSAGE_FOR_EVERYONE_MUTATION,SET_MESSAGE_STARRED_MUTATION,KEEP_MESSAGE_MUTATION,VOTE_POLL_MUTATION,CREATE_POLL_MUTATION,TYPING_SUBSCRIPTION_WS,SEND_TYPING_MUTATION,VIEW_CHANNEL_MUTATION,CHANNEL_READ_STATE_QUERY}from'./chatOperations.js';import {mergeConversations,toConversation,sortConversations,patchConversationAfterUpdate,patchConversationAfterPost,mergeMessages,toChatMessage,reconcileOptimistic,ATTACHMENT_LABELS,applyReactionSummary,withViewerReaction,withViewerPollVote,readWatermarkOf,deliveredWatermarkOf}from'./chatModel.js';import {NATIVE_CHAT_CHANNELS_QUERY_VARS,isNativeMessengerChannel,NATIVE_CHAT_LIST_PAGE_SIZE,isMongoAccountId}from'./nativeChatChannelQuery.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -52,6 +52,7 @@ function useConversations(services, viewer, opts) {
52
52
  const [error, setError] = React.useState(null);
53
53
  const [hasMore, setHasMore] = React.useState(false);
54
54
  const [loadingMore, setLoadingMore] = React.useState(false);
55
+ const [blockedPeers, setBlockedPeers] = React.useState(() => /* @__PURE__ */ new Set());
55
56
  const viewerId = (_a = viewer == null ? void 0 : viewer.userId) != null ? _a : null;
56
57
  const mounted = React.useRef(true);
57
58
  const conversationsRef = React.useRef([]);
@@ -253,6 +254,144 @@ function useConversations(services, viewer, opts) {
253
254
  setConversations((prev) => mergeConversations(prev, [toConversation(channel, viewerId)]));
254
255
  return String(channel.id);
255
256
  }, [gql, viewerId, findExistingDirect]);
257
+ const runGroupMutation = React.useCallback(async (op, variables) => {
258
+ const res = await gql.mutate(op, variables);
259
+ const message = firstErrorMessage(res.errors);
260
+ if (message) {
261
+ setError(message);
262
+ return null;
263
+ }
264
+ const channel = res.data ? Object.values(res.data)[0] : null;
265
+ if (!(channel == null ? void 0 : channel.id) || !viewerId) {
266
+ setError("Something went wrong. Please try again.");
267
+ return null;
268
+ }
269
+ setConversations((prev) => mergeConversations(prev, [toConversation(channel, viewerId)]));
270
+ return String(channel.id);
271
+ }, [gql, viewerId]);
272
+ const createGroup = React.useCallback((name, memberIds, description) => runGroupMutation(CREATE_GROUP_CHANNEL_MUTATION, {
273
+ name,
274
+ memberIds,
275
+ description: description != null ? description : null
276
+ }), [runGroupMutation]);
277
+ const addGroupMembers = React.useCallback((channelId, memberIds) => runGroupMutation(ADD_GROUP_MEMBERS_MUTATION, {
278
+ channelId,
279
+ memberIds
280
+ }), [runGroupMutation]);
281
+ const removeGroupMember = React.useCallback((channelId, memberId) => runGroupMutation(REMOVE_GROUP_MEMBER_MUTATION, {
282
+ channelId,
283
+ memberId
284
+ }), [runGroupMutation]);
285
+ const setGroupAdmin = React.useCallback((channelId, memberId, admin) => runGroupMutation(SET_GROUP_ADMIN_MUTATION, {
286
+ channelId,
287
+ memberId,
288
+ admin
289
+ }), [runGroupMutation]);
290
+ const updateGroupInfo = React.useCallback((channelId, changes) => {
291
+ var _a2, _b;
292
+ return runGroupMutation(UPDATE_GROUP_INFO_MUTATION, {
293
+ channelId,
294
+ name: (_a2 = changes.name) != null ? _a2 : null,
295
+ description: (_b = changes.description) != null ? _b : null
296
+ });
297
+ }, [runGroupMutation]);
298
+ const setGroupSendPolicy = React.useCallback((channelId, onlyAdmins) => runGroupMutation(SET_GROUP_SEND_POLICY_MUTATION, {
299
+ channelId,
300
+ onlyAdmins
301
+ }), [runGroupMutation]);
302
+ const leaveGroup = React.useCallback(async (channelId) => {
303
+ const res = await gql.mutate(LEAVE_GROUP_MUTATION, {
304
+ channelId
305
+ });
306
+ const message = firstErrorMessage(res.errors);
307
+ if (message) {
308
+ setError(message);
309
+ return false;
310
+ }
311
+ setConversations((prev) => prev.filter((c) => c.id !== channelId));
312
+ return true;
313
+ }, [gql]);
314
+ const setDisappearing = React.useCallback((channelId, seconds) => runGroupMutation(SET_DISAPPEARING_MUTATION, {
315
+ channelId,
316
+ seconds: Math.max(0, Math.floor(seconds))
317
+ }), [runGroupMutation]);
318
+ React.useEffect(() => {
319
+ if (!viewerId || !listQueryEnabled) return;
320
+ let cancelled = false;
321
+ void (async () => {
322
+ var _a2;
323
+ const res = await gql.query(BLOCKED_CONTACTS_QUERY, {}, {
324
+ fetchPolicy: "network-only"
325
+ }).catch(() => null);
326
+ const raw = (_a2 = res == null ? void 0 : res.data) == null ? void 0 : _a2.blockedContacts;
327
+ const rows = Array.isArray(raw) ? raw : [];
328
+ if (cancelled) return;
329
+ const ids = rows.map((r) => {
330
+ var _a3;
331
+ return String((_a3 = r == null ? void 0 : r.userId) != null ? _a3 : "");
332
+ }).filter(Boolean);
333
+ setBlockedPeers(new Set(ids));
334
+ })();
335
+ return () => {
336
+ cancelled = true;
337
+ };
338
+ }, [gql, viewerId, listQueryEnabled]);
339
+ const isPeerBlocked = React.useCallback((userId) => Boolean(userId) && blockedPeers.has(String(userId)), [blockedPeers]);
340
+ const blockContact = React.useCallback(async (userId) => {
341
+ if (!userId) return false;
342
+ setBlockedPeers((prev) => new Set(prev).add(String(userId)));
343
+ const res = await gql.mutate(BLOCK_CONTACT_MUTATION, {
344
+ userId
345
+ });
346
+ const message = firstErrorMessage(res.errors);
347
+ if (message) {
348
+ setBlockedPeers((prev) => {
349
+ const next = new Set(prev);
350
+ next.delete(String(userId));
351
+ return next;
352
+ });
353
+ setError(message);
354
+ return false;
355
+ }
356
+ return true;
357
+ }, [gql]);
358
+ const unblockContact = React.useCallback(async (userId) => {
359
+ if (!userId) return false;
360
+ setBlockedPeers((prev) => {
361
+ const next = new Set(prev);
362
+ next.delete(String(userId));
363
+ return next;
364
+ });
365
+ const res = await gql.mutate(UNBLOCK_CONTACT_MUTATION, {
366
+ userId
367
+ });
368
+ const message = firstErrorMessage(res.errors);
369
+ if (message) {
370
+ setBlockedPeers((prev) => new Set(prev).add(String(userId)));
371
+ setError(message);
372
+ return false;
373
+ }
374
+ return true;
375
+ }, [gql]);
376
+ const reportTarget = React.useCallback(async (targetType, targetId, reportOpts) => {
377
+ var _a2, _b;
378
+ if (!targetId) return false;
379
+ const res = await gql.mutate(REPORT_TARGET_MUTATION, {
380
+ targetType,
381
+ targetId,
382
+ reason: (_a2 = reportOpts == null ? void 0 : reportOpts.reason) != null ? _a2 : null,
383
+ alsoBlock: (_b = reportOpts == null ? void 0 : reportOpts.alsoBlock) != null ? _b : false
384
+ });
385
+ const message = firstErrorMessage(res.errors);
386
+ if (message) {
387
+ setError(message);
388
+ return false;
389
+ }
390
+ if ((reportOpts == null ? void 0 : reportOpts.alsoBlock) && targetType === "CONTACT") {
391
+ setBlockedPeers((prev) => new Set(prev).add(String(targetId)));
392
+ }
393
+ return true;
394
+ }, [gql]);
256
395
  return {
257
396
  conversations,
258
397
  loading,
@@ -269,7 +408,19 @@ function useConversations(services, viewer, opts) {
269
408
  applyConversationUpdate,
270
409
  markDelivered,
271
410
  applyConversationMessage,
272
- openDirect
411
+ openDirect,
412
+ createGroup,
413
+ addGroupMembers,
414
+ removeGroupMember,
415
+ setGroupAdmin,
416
+ updateGroupInfo,
417
+ setGroupSendPolicy,
418
+ leaveGroup,
419
+ setDisappearing,
420
+ blockContact,
421
+ unblockContact,
422
+ reportTarget,
423
+ isPeerBlocked
273
424
  };
274
425
  }
275
426
  function useThread(gql, channelId, viewer, opts) {
@@ -359,10 +510,11 @@ function useThread(gql, channelId, viewer, opts) {
359
510
  error: (err) => mounted.current && setError(err.message)
360
511
  });
361
512
  }, [gql, channelId, viewerId, notifyList]);
362
- const send = React.useCallback(async (body, replyTo) => {
513
+ const send = React.useCallback(async (body, replyTo, mentions) => {
363
514
  var _a2, _b, _c;
364
515
  const text = body.trim();
365
516
  if (!text || !channelId || !viewerId) return;
517
+ const mentionIds = (mentions != null ? mentions : []).filter(Boolean);
366
518
  const clientId = `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
367
519
  const optimistic = {
368
520
  id: clientId,
@@ -377,11 +529,13 @@ function useThread(gql, channelId, viewer, opts) {
377
529
  attachments: [],
378
530
  reactions: [],
379
531
  replyTo: replyTo != null ? replyTo : null,
532
+ mentions: mentionIds,
380
533
  sendState: "sending"
381
534
  };
382
535
  pending.current.set(clientId, {
383
536
  body: text,
384
- replyTo
537
+ replyTo,
538
+ mentions: mentionIds
385
539
  });
386
540
  setMessages((prev) => mergeMessages(prev, [optimistic]));
387
541
  onConversationMessage == null ? void 0 : onConversationMessage({
@@ -392,15 +546,18 @@ function useThread(gql, channelId, viewer, opts) {
392
546
  id: viewerId
393
547
  }
394
548
  });
549
+ const extraProps = __spreadValues(__spreadValues({}, replyTo ? {
550
+ replyTo
551
+ } : {}), mentionIds.length ? {
552
+ mentions: mentionIds
553
+ } : {});
395
554
  const res = await gql.mutate(SEND_MESSAGE_MUTATION, {
396
555
  channelId,
397
- // extraProps land in post.props, which every client reads the quote back from.
556
+ // extraProps land in post.props, which every client reads the quote/mentions back from.
398
557
  messageInput: __spreadValues({
399
558
  content: text
400
- }, replyTo ? {
401
- extraProps: {
402
- replyTo
403
- }
559
+ }, Object.keys(extraProps).length ? {
560
+ extraProps
404
561
  } : {})
405
562
  });
406
563
  if (!mounted.current) return;
@@ -535,7 +692,7 @@ function useThread(gql, channelId, viewer, opts) {
535
692
  pending.current.delete(messageId);
536
693
  setMessages((prev) => prev.filter((m) => m.id !== messageId));
537
694
  if (((_a2 = entry.files) == null ? void 0 : _a2.length) && entry.kind) void sendAttachments(entry.files, entry.body, entry.kind, entry.replyTo);
538
- else void send(entry.body, entry.replyTo);
695
+ else void send(entry.body, entry.replyTo, entry.mentions);
539
696
  }, [send, sendAttachments]);
540
697
  const loadMore = React.useCallback(() => {
541
698
  if (loadingMore || !hasMore || loading) return;
@@ -598,6 +755,11 @@ function useThread(gql, channelId, viewer, opts) {
598
755
  const post = data == null ? void 0 : data.chatMessageUpdated;
599
756
  if (!(post == null ? void 0 : post.id) || !mounted.current) return;
600
757
  const updated = toChatMessage(post, viewerId);
758
+ if (updated.disappeared) {
759
+ setMessages((prev) => prev.filter((m) => m.id !== updated.id));
760
+ onConversationUpdate == null ? void 0 : onConversationUpdate(post);
761
+ return;
762
+ }
601
763
  setMessages((prev) => prev.map((m) => m.id === updated.id ? __spreadProps(__spreadValues({}, updated), {
602
764
  sendState: m.sendState
603
765
  }) : m));
@@ -703,6 +865,79 @@ function useThread(gql, channelId, viewer, opts) {
703
865
  }
704
866
  })();
705
867
  }, [gql, channelId, viewerId, messageIdOf]);
868
+ const keep = React.useCallback((messageId, keepIt) => {
869
+ if (!channelId || !viewerId) return;
870
+ const before = messagesRef.current.find((m) => m.id === messageId);
871
+ if (!before) return;
872
+ setMessages((prev) => prev.map((m) => m.id === messageId ? __spreadProps(__spreadValues({}, m), {
873
+ kept: keepIt
874
+ }) : m));
875
+ void (async () => {
876
+ var _a2, _b, _c;
877
+ const res = await gql.mutate(KEEP_MESSAGE_MUTATION, {
878
+ messageId: messageIdOf(messageId),
879
+ keep: keepIt
880
+ });
881
+ if (!mounted.current) return;
882
+ const saved = (_a2 = res.data) == null ? void 0 : _a2.keepMessage;
883
+ if (((_b = res.errors) == null ? void 0 : _b.length) || !saved) {
884
+ setMessages((prev) => prev.map((m) => m.id === messageId ? before : m));
885
+ setError((_c = firstErrorMessage(res.errors)) != null ? _c : "Could not update the message.");
886
+ return;
887
+ }
888
+ setMessages((prev) => prev.map((m) => m.id === messageId ? toChatMessage(saved, viewerId) : m));
889
+ })();
890
+ }, [gql, channelId, viewerId, messageIdOf]);
891
+ const vote = React.useCallback((messageId, optionId) => {
892
+ if (!channelId || !viewerId) return;
893
+ const target = messagesRef.current.find((m) => m.id === messageId);
894
+ if (!(target == null ? void 0 : target.poll)) return;
895
+ const before = target.poll;
896
+ const after = withViewerPollVote(before, optionId, viewerId);
897
+ const optionIds = after.options.filter((o) => o.mine).map((o) => o.id);
898
+ setMessages((prev) => prev.map((m) => m.id === messageId ? __spreadProps(__spreadValues({}, m), {
899
+ poll: after
900
+ }) : m));
901
+ void (async () => {
902
+ var _a2, _b, _c;
903
+ const res = await gql.mutate(VOTE_POLL_MUTATION, {
904
+ channelId,
905
+ postId: messageId,
906
+ optionIds
907
+ });
908
+ if (!mounted.current) return;
909
+ const saved = (_a2 = res.data) == null ? void 0 : _a2.votePoll;
910
+ if (((_b = res.errors) == null ? void 0 : _b.length) || !saved) {
911
+ setMessages((prev) => prev.map((m) => m.id === messageId ? __spreadProps(__spreadValues({}, m), {
912
+ poll: before
913
+ }) : m));
914
+ setError((_c = firstErrorMessage(res.errors)) != null ? _c : "Could not record your vote.");
915
+ return;
916
+ }
917
+ setMessages((prev) => prev.map((m) => m.id === messageId ? toChatMessage(saved, viewerId) : m));
918
+ })();
919
+ }, [gql, channelId, viewerId]);
920
+ const createPoll = React.useCallback(async (question, options, multiple) => {
921
+ var _a2, _b, _c;
922
+ if (!channelId || !viewerId) return;
923
+ const q = question.trim();
924
+ const opts2 = options.map((o) => o.trim()).filter(Boolean);
925
+ if (!q || opts2.length < 2) return;
926
+ const res = await gql.mutate(CREATE_POLL_MUTATION, {
927
+ channelId,
928
+ question: q,
929
+ options: opts2,
930
+ multiple
931
+ });
932
+ if (!mounted.current) return;
933
+ const saved = (_a2 = res.data) == null ? void 0 : _a2.createPoll;
934
+ if (((_b = res.errors) == null ? void 0 : _b.length) || !saved) {
935
+ setError((_c = firstErrorMessage(res.errors)) != null ? _c : "Could not create the poll.");
936
+ return;
937
+ }
938
+ setMessages((prev) => mergeMessages(prev, [toChatMessage(saved, viewerId)]));
939
+ notifyList(saved);
940
+ }, [gql, channelId, viewerId, notifyList]);
706
941
  const loadUntil = React.useCallback(async (messageId) => {
707
942
  for (let page = 0; page < MAX_PAGES_TO_FIND; page += 1) {
708
943
  if (messagesRef.current.some((m) => m.id === messageId)) return true;
@@ -729,6 +964,9 @@ function useThread(gql, channelId, viewer, opts) {
729
964
  deleteForEveryone,
730
965
  deleteForMe,
731
966
  star,
967
+ keep,
968
+ createPoll,
969
+ vote,
732
970
  loadUntil
733
971
  };
734
972
  }