@adminide-stack/yantra-mobile 12.0.70-alpha.4 → 12.0.70
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.
- package/lib/config/env-config.js +1 -1
- package/lib/config/env-config.js.map +1 -1
- package/lib/features/messaging/ChatAttachments.js +383 -0
- package/lib/features/messaging/ChatAttachments.js.map +1 -0
- package/lib/features/messaging/ChatComponents.js +22 -6
- package/lib/features/messaging/ChatComponents.js.map +1 -1
- package/lib/features/messaging/ContactsScreen.js +15 -16
- package/lib/features/messaging/ContactsScreen.js.map +1 -1
- package/lib/features/messaging/NativeChatExperience.js +5 -1
- package/lib/features/messaging/NativeChatExperience.js.map +1 -1
- package/lib/features/messaging/ThreadScreen.js +88 -13
- package/lib/features/messaging/ThreadScreen.js.map +1 -1
- package/lib/features/messaging/chatModel.js +93 -7
- package/lib/features/messaging/chatModel.js.map +1 -1
- package/lib/features/messaging/chatOperations.js +27 -6
- package/lib/features/messaging/chatOperations.js.map +1 -1
- package/lib/features/messaging/useChatData.js +125 -6
- package/lib/features/messaging/useChatData.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatModel.js","sources":["../../../src/features/messaging/chatModel.ts"],"sourcesContent":["/**\n * Pure shaping between messenger-box's wire types and what the chat UI renders.\n *\n * Everything here is a plain function over plain data - no React, no seams, no\n * clock beyond what is passed in - because this is where the bugs that actually\n * ruin a chat client live: messages in the wrong order, a duplicate after a\n * reconnect, an optimistic row that never resolves, a DM titled with your own\n * name. Those are all cheap to unit-test and expensive to debug on a device.\n */\n\nexport interface ChatUser {\n id: string;\n givenName?: string | null;\n familyName?: string | null;\n username?: string | null;\n email?: string | null;\n picture?: string | null;\n profile?: { displayName?: string | null; picture?: string | null } | null;\n}\n\nexport interface WirePost {\n id: string;\n message?: string | null;\n type?: string | null;\n createdAt?: string | null;\n updatedAt?: string | null;\n editedAt?: string | null;\n deletedAt?: string | null;\n rootId?: string | null;\n parentId?: string | null;\n filenames?: (string | null)[] | null;\n props?: Record<string, unknown> | null;\n author?: ChatUser | null;\n creator?: ChatUser | null;\n reactionSummary?: WireReaction[] | null;\n}\n\nexport interface WireReaction {\n reaction: string;\n count: number;\n userIds?: (string | null)[] | null;\n}\n\n/** One Tapback on a message, grouped across users. */\nexport interface ChatReaction {\n reaction: string;\n count: number;\n /** The viewer is one of the reactors. */\n mine: boolean;\n userIds: string[];\n}\n\nexport interface WireChannelMember {\n id?: string | null;\n user?: ChatUser | null;\n msgCount?: number | null;\n lastViewedAt?: string | null;\n}\n\nexport interface WireChannel {\n id: string;\n type?: string | null;\n orgName?: string | null;\n title?: string | null;\n displayName?: string | null;\n topic?: string | null;\n lastPostAt?: string | null;\n totalMsgCount?: number | null;\n createdAt?: string | null;\n lastMessage?: WirePost | null;\n creator?: ChatUser | null;\n members?: (WireChannelMember | null)[] | null;\n}\n\n/** Where an optimistic message is in its lifecycle. */\nexport type ChatSendState = 'sending' | 'sent' | 'failed';\n\nexport interface ChatMessage {\n id: string;\n body: string;\n createdAt: number;\n authorId: string | null;\n authorName: string;\n authorAvatar?: string | null;\n isMine: boolean;\n isDeleted: boolean;\n isEdited: boolean;\n attachments: string[];\n /** Tapbacks, in first-use order. Empty when nobody reacted. */\n reactions: ChatReaction[];\n /** Present only on messages this device is still sending. */\n sendState?: ChatSendState;\n /** The optimistic id this row replaced, so the UI can animate rather than jump. */\n replacedClientId?: string;\n}\n\nexport interface ChatConversation {\n id: string;\n title: string;\n subtitle: string;\n avatarUrl?: string | null;\n /** Sort key; 0 when the channel has never had a message. */\n lastActivity: number;\n unreadCount: number;\n isDirect: boolean;\n /** The other participant, for a DM. Null for groups. */\n peer: ChatUser | null;\n}\n\n/**\n * A person's display name, in the order a human would expect.\n *\n * Email is the last resort and is truncated at the `@`: a transcript full of\n * `someone@example.com` is worse than one full of `someone`.\n */\nexport function displayNameOf(user?: ChatUser | null, fallback = 'Unknown'): string {\n if (!user) return fallback;\n const profileName = user.profile?.displayName?.trim();\n if (profileName) return profileName;\n const fullName = [user.givenName, user.familyName].filter(Boolean).join(' ').trim();\n if (fullName) return fullName;\n const username = user.username?.trim();\n if (username) return username;\n const email = user.email?.trim();\n if (email) return email.split('@')[0] || fallback;\n return fallback;\n}\n\nfunction memberNames(channel: WireChannel, viewerId: string): string[] {\n return (channel.members ?? [])\n .map((member) => member?.user)\n .filter((user): user is ChatUser => Boolean(user?.id) && String(user.id) !== viewerId)\n .map((user) => displayNameOf(user, ''))\n .filter(Boolean)\n .slice(0, 3);\n}\n\n/**\n * messenger-box populates `author` on some write paths and `creator` on others\n * (`sendMessage` sets creator). Reading only one shows \"Unknown\" for a large\n * fraction of real transcripts.\n */\nexport function pickAuthor(post: WirePost): ChatUser | null {\n return post.author ?? post.creator ?? null;\n}\n\n/** Epoch millis, tolerant of nulls and unparseable values. */\nexport function toEpoch(value?: string | number | null): number {\n if (value == null) return 0;\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nexport function toChatMessage(post: WirePost, viewerId: string): ChatMessage {\n const author = pickAuthor(post);\n return {\n id: String(post.id),\n body: post.message ?? '',\n createdAt: toEpoch(post.createdAt),\n authorId: author?.id ? String(author.id) : null,\n authorName: displayNameOf(author),\n authorAvatar: author?.picture ?? author?.profile?.picture ?? null,\n isMine: !!author?.id && String(author.id) === viewerId,\n isDeleted: !!post.deletedAt,\n isEdited: !!post.editedAt,\n attachments: (post.filenames ?? []).filter((f): f is string => typeof f === 'string' && f.length > 0),\n reactions: toChatReactions(post.reactionSummary, viewerId),\n };\n}\n\nexport function toChatReactions(summary: WireReaction[] | null | undefined, viewerId: string): ChatReaction[] {\n return (summary ?? [])\n .filter((r) => r && r.reaction && r.count > 0)\n .map((r) => {\n const userIds = (r.userIds ?? []).filter((u): u is string => typeof u === 'string' && u.length > 0);\n return { reaction: r.reaction, count: r.count, userIds, mine: userIds.includes(viewerId) };\n });\n}\n\n/** Replace one message's reactions with the server's latest summary. */\nexport function applyReactionSummary(\n messages: ChatMessage[],\n messageId: string,\n summary: WireReaction[] | null | undefined,\n viewerId: string,\n): ChatMessage[] {\n return messages.map((m) => (m.id === messageId ? { ...m, reactions: toChatReactions(summary, viewerId) } : m));\n}\n\n/**\n * The viewer's reactions after picking `emoji` (null = remove). One Tapback per person,\n * like iMessage: a new pick replaces the old one, the same pick removes it.\n */\nexport function withViewerReaction(reactions: ChatReaction[], emoji: string | null, viewerId: string): ChatReaction[] {\n const current = reactions.find((r) => r.mine)?.reaction ?? null;\n const next = emoji && emoji !== current ? emoji : null;\n const out = reactions\n .map((r) =>\n r.mine ? { ...r, mine: false, count: r.count - 1, userIds: r.userIds.filter((u) => u !== viewerId) } : r,\n )\n .filter((r) => r.count > 0);\n if (!next) return out;\n const idx = out.findIndex((r) => r.reaction === next);\n if (idx >= 0) {\n const r = out[idx];\n out[idx] = { ...r, mine: true, count: r.count + 1, userIds: [...r.userIds, viewerId] };\n return out;\n }\n return [...out, { reaction: next, count: 1, mine: true, userIds: [viewerId] }];\n}\n\n/**\n * Merge a batch of messages into the transcript: de-duplicated by id, oldest\n * first.\n *\n * De-duplication is the whole point. The same message arrives twice all the\n * time - the subscription delivers it while the head-page refetch that follows\n * a reconnect also returns it, and offset paging re-reads a row when something\n * lands mid-scroll. LAST WRITE WINS on a collision, so a server row replaces\n * the optimistic copy rather than the other way round.\n *\n * Ties break on id, not on insertion order: two messages can share a timestamp\n * to the millisecond, and an unstable comparator makes them swap places on\n * every render.\n */\nexport function mergeMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {\n const byId = new Map<string, ChatMessage>();\n for (const m of existing) byId.set(m.id, m);\n for (const m of incoming) byId.set(m.id, m);\n return Array.from(byId.values()).sort(\n (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\n );\n}\n\n/**\n * Reconcile a server message with the optimistic row it confirms.\n *\n * Matched on (author, body, nearby timestamp) rather than an echoed client id,\n * because `sendMessage` does not round-trip one. The window is deliberately\n * generous - a slow network is normal - and the cost of a miss is a brief\n * duplicate that the next refetch cleans up, not a lost message.\n */\nexport function reconcileOptimistic(messages: ChatMessage[], confirmed: ChatMessage, windowMs = 60_000): ChatMessage[] {\n // Only the viewer's OWN message can confirm the viewer's pending row.\n // Without this, someone else sending the identical text inside the window\n // consumes the optimistic slot: their message is relabelled as yours and\n // yours disappears. \"ok\", \"thanks\" and \"👍\" collide constantly.\n if (!confirmed.isMine) return mergeMessages(messages, [confirmed]);\n\n const pendingIdx = messages.findIndex(\n (m) =>\n m.sendState === 'sending' &&\n m.isMine &&\n m.body === confirmed.body &&\n // Same author, not merely both \"mine\" - guards the same collision\n // when authorId is populated on both sides.\n (!m.authorId || !confirmed.authorId || m.authorId === confirmed.authorId) &&\n Math.abs(m.createdAt - confirmed.createdAt) <= windowMs,\n );\n if (pendingIdx === -1) return mergeMessages(messages, [confirmed]);\n\n const next = messages.slice();\n next[pendingIdx] = { ...confirmed, sendState: 'sent', replacedClientId: messages[pendingIdx].id };\n // Still merge: the same id may already be present from a concurrent refetch.\n return mergeMessages(\n next.filter((m) => m.id !== confirmed.id || m.replacedClientId),\n [next[pendingIdx]],\n );\n}\n\n/**\n * The other participant in a DM.\n *\n * Falls back to null rather than to the viewer: a conversation labelled with\n * your own name is a bug users notice immediately, and every DM the viewer\n * created before members were populated would hit it.\n */\nexport function peerOf(channel: WireChannel, viewerId: string): ChatUser | null {\n const members = (channel.members ?? []).filter(Boolean) as WireChannelMember[];\n const other = members.find((m) => m.user?.id && String(m.user.id) !== viewerId);\n return other?.user ?? null;\n}\n\n/**\n * How many messages the viewer has not read.\n *\n * `member.msgCount` is the count the member has SEEN, so unread is the channel\n * total minus that - clamped at zero because the two counters are updated on\n * different write paths and briefly disagree.\n */\nexport function unreadOf(channel: WireChannel, viewerId: string): number {\n const mine = (channel.members ?? []).find((m) => m?.user?.id && String(m.user.id) === viewerId);\n if (!mine) return 0;\n const total = channel.totalMsgCount ?? 0;\n const seen = mine.msgCount ?? 0;\n return Math.max(0, total - seen);\n}\n\nconst DIRECT_TYPES = new Set(['DIRECT', 'Direct', 'direct']);\n\nfunction looksLikeMachineTitle(value?: string | null): boolean {\n const label = value?.trim() ?? '';\n if (!label) return true;\n if (/^chat[\\s_-]*channel\\b/i.test(label) || /^chat-channel-/i.test(label)) return true;\n if (/^[a-f0-9]{8,}$/i.test(label)) return true;\n if (label.includes('__')) return true;\n if (/^group(\\s+chat)?$/i.test(label)) return true;\n return false;\n}\n\nexport function toConversation(channel: WireChannel, viewerId: string): ChatConversation {\n const peerCandidate = peerOf(channel, viewerId);\n const explicitDirect = DIRECT_TYPES.has(String(channel.type ?? ''));\n const populatedMembers = (channel.members ?? []).filter(Boolean);\n const twoParty = populatedMembers.length === 2 && Boolean(peerCandidate);\n const hasMachineLabel = looksLikeMachineTitle(channel.displayName) && looksLikeMachineTitle(channel.title);\n const isDirect = explicitDirect || twoParty || (!channel.type && hasMachineLabel && Boolean(peerCandidate));\n const peer = isDirect ? peerCandidate : null;\n\n const names = memberNames(channel, viewerId);\n const lastAuthor = channel.lastMessage ? pickAuthor(channel.lastMessage) : null;\n const lastAuthorName = lastAuthor && String(lastAuthor.id) !== viewerId ? displayNameOf(lastAuthor, '') : '';\n const creatorName = displayNameOf(channel.creator, '');\n const groupFallback = names.length\n ? names.join(', ')\n : lastAuthorName || (creatorName && creatorName !== 'Unknown' ? creatorName : 'Chat');\n const storedLabel = !looksLikeMachineTitle(channel.displayName)\n ? channel.displayName!.trim()\n : !looksLikeMachineTitle(channel.title)\n ? channel.title!.trim()\n : '';\n const title = isDirect\n ? displayNameOf(peer, storedLabel || lastAuthorName || 'Direct message')\n : storedLabel || groupFallback;\n const topic = looksLikeMachineTitle(channel.topic) ? '' : (channel.topic ?? '').trim();\n const preview = channel.lastMessage?.message?.trim() || topic;\n\n return {\n id: String(channel.id),\n title,\n subtitle: preview === title ? '' : preview,\n avatarUrl:\n peer?.picture ??\n peer?.profile?.picture ??\n channel.members?.find((member) => member?.user?.picture)?.user?.picture ??\n lastAuthor?.picture ??\n lastAuthor?.profile?.picture ??\n null,\n lastActivity: toEpoch(channel.lastPostAt) || toEpoch(channel.createdAt),\n unreadCount: unreadOf(channel, viewerId),\n isDirect,\n peer,\n };\n}\n\n/**\n * Conversation list order: most recent first, then title, then id.\n *\n * The trailing keys matter more than they look. A channel with no messages has\n * `lastActivity === 0`, so a fresh account is entirely ties; without a stable\n * tiebreak the list reshuffles on every poll.\n */\nexport function sortConversations(list: ChatConversation[]): ChatConversation[] {\n return list\n .slice()\n .sort(\n (a, b) =>\n b.lastActivity - a.lastActivity ||\n a.title.localeCompare(b.title) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\n );\n}\n\n/** Stable key for collapsing duplicate list rows (same person, multiple channel ids). */\nexport function conversationListKey(c: ChatConversation): string {\n if (c.peer?.id) return `peer:${String(c.peer.id)}`;\n const title = c.title.trim().toLowerCase();\n if (!title || title === 'chat' || title === 'unknown') return `id:${c.id}`;\n // Multi-person groups use comma-separated titles; keep those distinct.\n if (title.includes(',')) return `id:${c.id}`;\n if (c.isDirect) return `title:${title}`;\n // Server often returns duplicate 1:1 channels as untyped GROUP rows with the peer's name.\n return `title:${title}`;\n}\n\n/** Newest row wins per list key. */\nexport function dedupeConversationList(list: ChatConversation[]): ChatConversation[] {\n const sorted = sortConversations(list);\n const seen = new Set<string>();\n const out: ChatConversation[] = [];\n for (const c of sorted) {\n const key = conversationListKey(c);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(c);\n }\n return out;\n}\n\n/** @deprecated Use dedupeConversationList */\nexport function dedupeDirectConversations(list: ChatConversation[]): ChatConversation[] {\n return dedupeConversationList(list);\n}\n\nexport function mergeConversations(existing: ChatConversation[], incoming: ChatConversation[]): ChatConversation[] {\n const byId = new Map<string, ChatConversation>();\n for (const c of existing) byId.set(c.id, c);\n for (const c of incoming) byId.set(c.id, c);\n return dedupeConversationList(Array.from(byId.values()));\n}\n\n/** Refresh preview + sort key when a new post lands (send echo or subscription). */\nexport function patchConversationAfterPost(conv: ChatConversation, post: WirePost, viewerId: string): ChatConversation {\n const preview = post.message?.trim();\n const nextSubtitle = preview == null || preview === '' ? conv.subtitle : preview === conv.title ? '' : preview;\n const activity = toEpoch(post.createdAt) || Date.now();\n const author = pickAuthor(post);\n const fromPeer = author && String(author.id) !== viewerId;\n return {\n ...conv,\n subtitle: nextSubtitle,\n lastActivity: activity,\n unreadCount: fromPeer ? conv.unreadCount + 1 : conv.unreadCount,\n };\n}\n\n/**\n * Day separators, computed against a caller-supplied \"now\".\n *\n * The clock is a parameter so this is testable and so a device with a skewed\n * clock does not silently label everything \"Today\".\n */\nexport function dayLabel(epoch: number, now: number): string {\n if (!epoch) return '';\n const d = new Date(epoch);\n const n = new Date(now);\n const sameDay = (a: Date, b: Date) =>\n a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n if (sameDay(d, n)) return 'Today';\n const yesterday = new Date(now);\n yesterday.setDate(yesterday.getDate() - 1);\n if (sameDay(d, yesterday)) return 'Yesterday';\n return d.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: d.getFullYear() === n.getFullYear() ? undefined : 'numeric',\n });\n}\n\n/** Short clock label for a bubble. */\nexport function timeLabel(epoch: number): string {\n if (!epoch) return '';\n return new Date(epoch).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });\n}\n\n/** Relative age for the conversation list. */\nexport function relativeLabel(epoch: number, now: number): string {\n if (!epoch) return '';\n const diff = now - epoch;\n if (diff < 60_000) return 'now';\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;\n const d = new Date(epoch);\n const n = new Date(now);\n if (d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate()) {\n return timeLabel(epoch);\n }\n if (diff < 7 * 86_400_000) return d.toLocaleDateString(undefined, { weekday: 'short' });\n return d.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });\n}\n\n/** Initials for the avatar fallback; never more than two glyphs. */\nexport function initialsOf(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (!parts.length) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chatModel.js","sources":["../../../src/features/messaging/chatModel.ts"],"sourcesContent":["/**\n * Pure shaping between messenger-box's wire types and what the chat UI renders.\n *\n * Everything here is a plain function over plain data - no React, no seams, no\n * clock beyond what is passed in - because this is where the bugs that actually\n * ruin a chat client live: messages in the wrong order, a duplicate after a\n * reconnect, an optimistic row that never resolves, a DM titled with your own\n * name. Those are all cheap to unit-test and expensive to debug on a device.\n */\n\nexport interface ChatUser {\n id: string;\n givenName?: string | null;\n familyName?: string | null;\n username?: string | null;\n email?: string | null;\n picture?: string | null;\n profile?: { displayName?: string | null; picture?: string | null } | null;\n}\n\nexport interface WirePost {\n id: string;\n message?: string | null;\n type?: string | null;\n createdAt?: string | null;\n updatedAt?: string | null;\n editedAt?: string | null;\n deletedAt?: string | null;\n rootId?: string | null;\n parentId?: string | null;\n filenames?: (string | null)[] | null;\n props?: Record<string, unknown> | null;\n /** Attached files (messenger-box `Post.files`, linked to the post by id). */\n files?: { data?: (WireFile | null)[] | null } | null;\n author?: ChatUser | null;\n creator?: ChatUser | null;\n reactionSummary?: WireReaction[] | null;\n}\n\nexport interface WireFile {\n id?: string | null;\n name?: string | null;\n mimeType?: string | null;\n url?: string | null;\n width?: number | null;\n height?: number | null;\n size?: number | null;\n}\n\n/** What a message's `props.attachmentKind` says the attachments are (set when sending). */\nexport type ChatAttachmentKind = 'image' | 'audio' | 'file';\n\n/**\n * One attached file. `url` is what the server returned (an image-resizer thumbnail for every\n * type) or, while sending, the local file; `key` is the storage key, from which the UI builds\n * the full-size / playable CDN URL (see `attachmentUrl`).\n */\nexport interface ChatAttachment {\n id: string;\n kind: ChatAttachmentKind;\n name: string;\n mimeType: string;\n url: string;\n key: string | null;\n width?: number | null;\n height?: number | null;\n /** Voice notes: recorded length (from the message's props). */\n durationMs?: number | null;\n}\n\nexport interface WireReaction {\n reaction: string;\n count: number;\n userIds?: (string | null)[] | null;\n}\n\n/** One Tapback on a message, grouped across users. */\nexport interface ChatReaction {\n reaction: string;\n count: number;\n /** The viewer is one of the reactors. */\n mine: boolean;\n userIds: string[];\n}\n\nexport interface WireChannelMember {\n id?: string | null;\n user?: ChatUser | null;\n msgCount?: number | null;\n lastViewedAt?: string | null;\n}\n\nexport interface WireChannel {\n id: string;\n type?: string | null;\n orgName?: string | null;\n title?: string | null;\n displayName?: string | null;\n topic?: string | null;\n lastPostAt?: string | null;\n totalMsgCount?: number | null;\n createdAt?: string | null;\n lastMessage?: WirePost | null;\n creator?: ChatUser | null;\n members?: (WireChannelMember | null)[] | null;\n}\n\n/** Where an optimistic message is in its lifecycle. */\nexport type ChatSendState = 'sending' | 'sent' | 'failed';\n\nexport interface ChatMessage {\n id: string;\n body: string;\n createdAt: number;\n authorId: string | null;\n authorName: string;\n authorAvatar?: string | null;\n isMine: boolean;\n isDeleted: boolean;\n isEdited: boolean;\n attachments: ChatAttachment[];\n /** Tapbacks, in first-use order. Empty when nobody reacted. */\n reactions: ChatReaction[];\n /** Present only on messages this device is still sending. */\n sendState?: ChatSendState;\n /** The optimistic id this row replaced, so the UI can animate rather than jump. */\n replacedClientId?: string;\n}\n\nexport interface ChatConversation {\n id: string;\n title: string;\n subtitle: string;\n avatarUrl?: string | null;\n /** Sort key; 0 when the channel has never had a message. */\n lastActivity: number;\n unreadCount: number;\n isDirect: boolean;\n /** The other participant, for a DM. Null for groups. */\n peer: ChatUser | null;\n}\n\n/**\n * A person's display name, in the order a human would expect.\n *\n * Email is the last resort and is truncated at the `@`: a transcript full of\n * `someone@example.com` is worse than one full of `someone`.\n */\nexport function displayNameOf(user?: ChatUser | null, fallback = 'Unknown'): string {\n if (!user) return fallback;\n const profileName = user.profile?.displayName?.trim();\n if (profileName) return profileName;\n const fullName = [user.givenName, user.familyName].filter(Boolean).join(' ').trim();\n if (fullName) return fullName;\n const username = user.username?.trim();\n if (username) return username;\n const email = user.email?.trim();\n if (email) return email.split('@')[0] || fallback;\n return fallback;\n}\n\nfunction memberNames(channel: WireChannel, viewerId: string): string[] {\n return (channel.members ?? [])\n .map((member) => member?.user)\n .filter((user): user is ChatUser => Boolean(user?.id) && String(user.id) !== viewerId)\n .map((user) => displayNameOf(user, ''))\n .filter(Boolean)\n .slice(0, 3);\n}\n\n/**\n * messenger-box populates `author` on some write paths and `creator` on others\n * (`sendMessage` sets creator). Reading only one shows \"Unknown\" for a large\n * fraction of real transcripts.\n */\nexport function pickAuthor(post: WirePost): ChatUser | null {\n return post.author ?? post.creator ?? null;\n}\n\n/** Epoch millis, tolerant of nulls and unparseable values. */\nexport function toEpoch(value?: string | number | null): number {\n if (value == null) return 0;\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nexport function toChatMessage(post: WirePost, viewerId: string): ChatMessage {\n const author = pickAuthor(post);\n const attachments = toChatAttachments(post);\n return {\n id: String(post.id),\n body: bodyOf(post, attachments.length > 0),\n createdAt: toEpoch(post.createdAt),\n authorId: author?.id ? String(author.id) : null,\n authorName: displayNameOf(author),\n authorAvatar: author?.picture ?? author?.profile?.picture ?? null,\n isMine: !!author?.id && String(author.id) === viewerId,\n isDeleted: !!post.deletedAt,\n isEdited: !!post.editedAt,\n attachments,\n reactions: toChatReactions(post.reactionSummary, viewerId),\n };\n}\n\nfunction kindOfMime(mimeType: string): ChatAttachmentKind {\n if (mimeType.startsWith('image/')) return 'image';\n if (mimeType.startsWith('audio/')) return 'audio';\n return 'file';\n}\n\n/**\n * The storage key inside a file URL. messenger-box hands back image-resizer URLs\n * (`<handler>/fit-in/150x150/<key>`) for every attachment; a plain storage URL or a bare key\n * also works.\n */\nexport function fileKeyFromUrl(url: string | null | undefined): string | null {\n const value = (url ?? '').trim();\n if (!value) return null;\n const resized = value.match(/\\/fit-in\\/\\d+x\\d+\\/(.+)$/);\n if (resized) return decodeURIComponent(resized[1].split('?')[0]);\n if (!/^[a-z]+:\\/\\//i.test(value)) return value.split('?')[0];\n if (/^(file|content|ph|assets-library):/i.test(value)) return null;\n try {\n return decodeURIComponent(new URL(value).pathname.replace(/^\\/+/, '')) || null;\n } catch {\n return null;\n }\n}\n\n/** Full-size (image) or playable (audio) URL: the CDN copy when a base is known, else what we have. */\nexport function attachmentUrl(attachment: ChatAttachment, fileBaseUrl?: string | null): string {\n if (!attachment.key || !fileBaseUrl) return attachment.url;\n return `${fileBaseUrl.replace(/\\/+$/, '')}/${attachment.key}`;\n}\n\n/** `props` is an AnyObject scalar: an object from some paths, a JSON string from others. */\nfunction propsOf(post: WirePost | null | undefined): Record<string, unknown> {\n const raw = post?.props as unknown;\n if (raw && typeof raw === 'object') return raw as Record<string, unknown>;\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === 'object' ? parsed : {};\n } catch {\n return {};\n }\n }\n return {};\n}\n\nexport function toChatAttachments(post: WirePost): ChatAttachment[] {\n const props = propsOf(post) as { attachmentKind?: unknown; voiceDurationMs?: unknown };\n const durationMs = typeof props.voiceDurationMs === 'number' ? props.voiceDurationMs : null;\n return (post.files?.data ?? [])\n .filter((f): f is WireFile => Boolean(f?.url))\n .map((f, i) => {\n const mimeType = f.mimeType ?? 'application/octet-stream';\n const kind = kindOfMime(mimeType);\n return {\n id: String(f.id ?? `${post.id}-${i}`),\n kind,\n name: f.name ?? 'file',\n mimeType,\n url: String(f.url),\n key: fileKeyFromUrl(f.url),\n width: f.width ?? null,\n height: f.height ?? null,\n durationMs: kind === 'audio' ? durationMs : null,\n };\n });\n}\n\n/**\n * The text of an attachment-only message. messenger-box requires non-blank message text, so this\n * label is sent as the text: push notifications and other clients read it, and this app hides it\n * inside the bubble (see `toChatMessage`).\n */\nexport const ATTACHMENT_LABELS: Record<ChatAttachmentKind, string> = {\n image: '📷 Photo',\n audio: '🎤 Voice message',\n file: '📎 Attachment',\n};\n\n/** Chat-list preview for a message that is only an attachment. */\nexport function attachmentPreview(post: WirePost | null | undefined): string {\n const kind = propsOf(post).attachmentKind;\n return typeof kind === 'string' && kind in ATTACHMENT_LABELS ? ATTACHMENT_LABELS[kind as ChatAttachmentKind] : '';\n}\n\nconst LABEL_TEXTS = new Set(Object.values(ATTACHMENT_LABELS));\n\n/** The bubble text: an attachment-only message's label is not shown next to the attachment itself. */\nfunction bodyOf(post: WirePost, hasAttachments: boolean): string {\n const body = post.message ?? '';\n const trimmed = body.trim();\n const label = attachmentPreview(post);\n if (label && trimmed === label) return '';\n // Same label with the files attached, even if props did not come back.\n return hasAttachments && LABEL_TEXTS.has(trimmed) ? '' : body;\n}\n\nexport function toChatReactions(summary: WireReaction[] | null | undefined, viewerId: string): ChatReaction[] {\n return (summary ?? [])\n .filter((r) => r && r.reaction && r.count > 0)\n .map((r) => {\n const userIds = (r.userIds ?? []).filter((u): u is string => typeof u === 'string' && u.length > 0);\n return { reaction: r.reaction, count: r.count, userIds, mine: userIds.includes(viewerId) };\n });\n}\n\n/** Replace one message's reactions with the server's latest summary. */\nexport function applyReactionSummary(\n messages: ChatMessage[],\n messageId: string,\n summary: WireReaction[] | null | undefined,\n viewerId: string,\n): ChatMessage[] {\n return messages.map((m) => (m.id === messageId ? { ...m, reactions: toChatReactions(summary, viewerId) } : m));\n}\n\n/**\n * The viewer's reactions after picking `emoji` (null = remove). One Tapback per person,\n * like iMessage: a new pick replaces the old one, the same pick removes it.\n */\nexport function withViewerReaction(reactions: ChatReaction[], emoji: string | null, viewerId: string): ChatReaction[] {\n const current = reactions.find((r) => r.mine)?.reaction ?? null;\n const next = emoji && emoji !== current ? emoji : null;\n const out = reactions\n .map((r) =>\n r.mine ? { ...r, mine: false, count: r.count - 1, userIds: r.userIds.filter((u) => u !== viewerId) } : r,\n )\n .filter((r) => r.count > 0);\n if (!next) return out;\n const idx = out.findIndex((r) => r.reaction === next);\n if (idx >= 0) {\n const r = out[idx];\n out[idx] = { ...r, mine: true, count: r.count + 1, userIds: [...r.userIds, viewerId] };\n return out;\n }\n return [...out, { reaction: next, count: 1, mine: true, userIds: [viewerId] }];\n}\n\n/**\n * Merge a batch of messages into the transcript: de-duplicated by id, oldest\n * first.\n *\n * De-duplication is the whole point. The same message arrives twice all the\n * time - the subscription delivers it while the head-page refetch that follows\n * a reconnect also returns it, and offset paging re-reads a row when something\n * lands mid-scroll. LAST WRITE WINS on a collision, so a server row replaces\n * the optimistic copy rather than the other way round.\n *\n * Ties break on id, not on insertion order: two messages can share a timestamp\n * to the millisecond, and an unstable comparator makes them swap places on\n * every render.\n */\nexport function mergeMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {\n const byId = new Map<string, ChatMessage>();\n for (const m of existing) byId.set(m.id, m);\n for (const m of incoming) byId.set(m.id, m);\n return Array.from(byId.values()).sort(\n (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\n );\n}\n\n/**\n * Reconcile a server message with the optimistic row it confirms.\n *\n * Matched on (author, body, nearby timestamp) rather than an echoed client id,\n * because `sendMessage` does not round-trip one. The window is deliberately\n * generous - a slow network is normal - and the cost of a miss is a brief\n * duplicate that the next refetch cleans up, not a lost message.\n */\nexport function reconcileOptimistic(messages: ChatMessage[], confirmed: ChatMessage, windowMs = 60_000): ChatMessage[] {\n // Only the viewer's OWN message can confirm the viewer's pending row.\n // Without this, someone else sending the identical text inside the window\n // consumes the optimistic slot: their message is relabelled as yours and\n // yours disappears. \"ok\", \"thanks\" and \"👍\" collide constantly.\n if (!confirmed.isMine) return mergeMessages(messages, [confirmed]);\n\n // Attachment sends pick the post id up front, so the pending row already has the real id.\n const sameIdIdx = messages.findIndex((m) => m.id === confirmed.id && m.sendState === 'sending');\n if (sameIdIdx !== -1) {\n const next = messages.slice();\n const pending = messages[sameIdIdx];\n next[sameIdIdx] = {\n ...confirmed,\n // Keep the local previews until the server copy lists its files.\n attachments: confirmed.attachments.length ? confirmed.attachments : pending.attachments,\n sendState: 'sent',\n };\n return next;\n }\n\n const pendingIdx = messages.findIndex(\n (m) =>\n m.sendState === 'sending' &&\n m.isMine &&\n m.body === confirmed.body &&\n // Same author, not merely both \"mine\" - guards the same collision\n // when authorId is populated on both sides.\n (!m.authorId || !confirmed.authorId || m.authorId === confirmed.authorId) &&\n Math.abs(m.createdAt - confirmed.createdAt) <= windowMs,\n );\n if (pendingIdx === -1) return mergeMessages(messages, [confirmed]);\n\n const next = messages.slice();\n next[pendingIdx] = { ...confirmed, sendState: 'sent', replacedClientId: messages[pendingIdx].id };\n // Still merge: the same id may already be present from a concurrent refetch.\n return mergeMessages(\n next.filter((m) => m.id !== confirmed.id || m.replacedClientId),\n [next[pendingIdx]],\n );\n}\n\n/**\n * The other participant in a DM.\n *\n * Falls back to null rather than to the viewer: a conversation labelled with\n * your own name is a bug users notice immediately, and every DM the viewer\n * created before members were populated would hit it.\n */\nexport function peerOf(channel: WireChannel, viewerId: string): ChatUser | null {\n const members = (channel.members ?? []).filter(Boolean) as WireChannelMember[];\n const other = members.find((m) => m.user?.id && String(m.user.id) !== viewerId);\n return other?.user ?? null;\n}\n\n/**\n * How many messages the viewer has not read.\n *\n * `member.msgCount` is the count the member has SEEN, so unread is the channel\n * total minus that - clamped at zero because the two counters are updated on\n * different write paths and briefly disagree.\n */\nexport function unreadOf(channel: WireChannel, viewerId: string): number {\n const mine = (channel.members ?? []).find((m) => m?.user?.id && String(m.user.id) === viewerId);\n if (!mine) return 0;\n const total = channel.totalMsgCount ?? 0;\n const seen = mine.msgCount ?? 0;\n return Math.max(0, total - seen);\n}\n\nconst DIRECT_TYPES = new Set(['DIRECT', 'Direct', 'direct']);\n\nfunction looksLikeMachineTitle(value?: string | null): boolean {\n const label = value?.trim() ?? '';\n if (!label) return true;\n if (/^chat[\\s_-]*channel\\b/i.test(label) || /^chat-channel-/i.test(label)) return true;\n if (/^[a-f0-9]{8,}$/i.test(label)) return true;\n if (label.includes('__')) return true;\n if (/^group(\\s+chat)?$/i.test(label)) return true;\n return false;\n}\n\nexport function toConversation(channel: WireChannel, viewerId: string): ChatConversation {\n const peerCandidate = peerOf(channel, viewerId);\n const explicitDirect = DIRECT_TYPES.has(String(channel.type ?? ''));\n const populatedMembers = (channel.members ?? []).filter(Boolean);\n const twoParty = populatedMembers.length === 2 && Boolean(peerCandidate);\n const hasMachineLabel = looksLikeMachineTitle(channel.displayName) && looksLikeMachineTitle(channel.title);\n const isDirect = explicitDirect || twoParty || (!channel.type && hasMachineLabel && Boolean(peerCandidate));\n const peer = isDirect ? peerCandidate : null;\n\n const names = memberNames(channel, viewerId);\n const lastAuthor = channel.lastMessage ? pickAuthor(channel.lastMessage) : null;\n const lastAuthorName = lastAuthor && String(lastAuthor.id) !== viewerId ? displayNameOf(lastAuthor, '') : '';\n const creatorName = displayNameOf(channel.creator, '');\n const groupFallback = names.length\n ? names.join(', ')\n : lastAuthorName || (creatorName && creatorName !== 'Unknown' ? creatorName : 'Chat');\n const storedLabel = !looksLikeMachineTitle(channel.displayName)\n ? channel.displayName!.trim()\n : !looksLikeMachineTitle(channel.title)\n ? channel.title!.trim()\n : '';\n const title = isDirect\n ? displayNameOf(peer, storedLabel || lastAuthorName || 'Direct message')\n : storedLabel || groupFallback;\n const topic = looksLikeMachineTitle(channel.topic) ? '' : (channel.topic ?? '').trim();\n const preview = channel.lastMessage?.message?.trim() || attachmentPreview(channel.lastMessage) || topic;\n\n return {\n id: String(channel.id),\n title,\n subtitle: preview === title ? '' : preview,\n avatarUrl:\n peer?.picture ??\n peer?.profile?.picture ??\n channel.members?.find((member) => member?.user?.picture)?.user?.picture ??\n lastAuthor?.picture ??\n lastAuthor?.profile?.picture ??\n null,\n lastActivity: toEpoch(channel.lastPostAt) || toEpoch(channel.createdAt),\n unreadCount: unreadOf(channel, viewerId),\n isDirect,\n peer,\n };\n}\n\n/**\n * Conversation list order: most recent first, then title, then id.\n *\n * The trailing keys matter more than they look. A channel with no messages has\n * `lastActivity === 0`, so a fresh account is entirely ties; without a stable\n * tiebreak the list reshuffles on every poll.\n */\nexport function sortConversations(list: ChatConversation[]): ChatConversation[] {\n return list\n .slice()\n .sort(\n (a, b) =>\n b.lastActivity - a.lastActivity ||\n a.title.localeCompare(b.title) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\n );\n}\n\n/** Stable key for collapsing duplicate list rows (same person, multiple channel ids). */\nexport function conversationListKey(c: ChatConversation): string {\n if (c.peer?.id) return `peer:${String(c.peer.id)}`;\n const title = c.title.trim().toLowerCase();\n if (!title || title === 'chat' || title === 'unknown') return `id:${c.id}`;\n // Multi-person groups use comma-separated titles; keep those distinct.\n if (title.includes(',')) return `id:${c.id}`;\n if (c.isDirect) return `title:${title}`;\n // Server often returns duplicate 1:1 channels as untyped GROUP rows with the peer's name.\n return `title:${title}`;\n}\n\n/** Newest row wins per list key. */\nexport function dedupeConversationList(list: ChatConversation[]): ChatConversation[] {\n const sorted = sortConversations(list);\n const seen = new Set<string>();\n const out: ChatConversation[] = [];\n for (const c of sorted) {\n const key = conversationListKey(c);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(c);\n }\n return out;\n}\n\n/** @deprecated Use dedupeConversationList */\nexport function dedupeDirectConversations(list: ChatConversation[]): ChatConversation[] {\n return dedupeConversationList(list);\n}\n\nexport function mergeConversations(existing: ChatConversation[], incoming: ChatConversation[]): ChatConversation[] {\n const byId = new Map<string, ChatConversation>();\n for (const c of existing) byId.set(c.id, c);\n for (const c of incoming) byId.set(c.id, c);\n return dedupeConversationList(Array.from(byId.values()));\n}\n\n/** Refresh preview + sort key when a new post lands (send echo or subscription). */\nexport function patchConversationAfterPost(conv: ChatConversation, post: WirePost, viewerId: string): ChatConversation {\n const preview = post.message?.trim() || attachmentPreview(post);\n const nextSubtitle = preview == null || preview === '' ? conv.subtitle : preview === conv.title ? '' : preview;\n const activity = toEpoch(post.createdAt) || Date.now();\n const author = pickAuthor(post);\n const fromPeer = author && String(author.id) !== viewerId;\n return {\n ...conv,\n subtitle: nextSubtitle,\n lastActivity: activity,\n unreadCount: fromPeer ? conv.unreadCount + 1 : conv.unreadCount,\n };\n}\n\n/**\n * Day separators, computed against a caller-supplied \"now\".\n *\n * The clock is a parameter so this is testable and so a device with a skewed\n * clock does not silently label everything \"Today\".\n */\nexport function dayLabel(epoch: number, now: number): string {\n if (!epoch) return '';\n const d = new Date(epoch);\n const n = new Date(now);\n const sameDay = (a: Date, b: Date) =>\n a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n if (sameDay(d, n)) return 'Today';\n const yesterday = new Date(now);\n yesterday.setDate(yesterday.getDate() - 1);\n if (sameDay(d, yesterday)) return 'Yesterday';\n return d.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: d.getFullYear() === n.getFullYear() ? undefined : 'numeric',\n });\n}\n\n/** Short clock label for a bubble. */\nexport function timeLabel(epoch: number): string {\n if (!epoch) return '';\n return new Date(epoch).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });\n}\n\n/** Relative age for the conversation list. */\nexport function relativeLabel(epoch: number, now: number): string {\n if (!epoch) return '';\n const diff = now - epoch;\n if (diff < 60_000) return 'now';\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;\n const d = new Date(epoch);\n const n = new Date(now);\n if (d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate()) {\n return timeLabel(epoch);\n }\n if (diff < 7 * 86_400_000) return d.toLocaleDateString(undefined, { weekday: 'short' });\n return d.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });\n}\n\n/** Initials for the avatar fallback; never more than two glyphs. */\nexport function initialsOf(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (!parts.length) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n"],"names":["_a","_b","next"],"mappings":"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"}
|
|
@@ -14,6 +14,10 @@ const POST_FIELDS = `
|
|
|
14
14
|
creator { id givenName familyName username email picture }
|
|
15
15
|
reactionSummary { reaction count userIds }
|
|
16
16
|
`;
|
|
17
|
+
const MESSAGE_FIELDS = `
|
|
18
|
+
${POST_FIELDS}
|
|
19
|
+
files { data { id name mimeType url width height size } }
|
|
20
|
+
`;
|
|
17
21
|
const CHANNEL_FIELDS = `
|
|
18
22
|
id
|
|
19
23
|
type
|
|
@@ -40,15 +44,32 @@ const MESSAGES_QUERY = `
|
|
|
40
44
|
messages(channelId: $channelId, limit: $limit, skip: $skip, sort: $sort) {
|
|
41
45
|
totalCount
|
|
42
46
|
data {
|
|
43
|
-
${
|
|
47
|
+
${MESSAGE_FIELDS}
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
`;
|
|
48
52
|
const SEND_MESSAGE_MUTATION = `
|
|
49
|
-
mutation ChatSendMessage($channelId: String!, $messageInput: MessageInput
|
|
50
|
-
sendMessage(channelId: $channelId, messageInput: $messageInput) {
|
|
51
|
-
${
|
|
53
|
+
mutation ChatSendMessage($channelId: String!, $messageInput: MessageInput!, $postId: ID) {
|
|
54
|
+
sendMessage(channelId: $channelId, messageInput: $messageInput, postId: $postId) {
|
|
55
|
+
${MESSAGE_FIELDS}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
const CREATE_MESSAGE_UPLOAD_LINKS_MUTATION = `
|
|
60
|
+
mutation ChatCreateMessageUploadLinks($postId: ID!, $filenames: [String!]!) {
|
|
61
|
+
createMessageFilesUploadLink(postId: $postId, filenames: $filenames)
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
const ATTACH_UPLOADED_FILES_MUTATION = `
|
|
65
|
+
mutation ChatAttachUploadedFiles($postId: ID!, $files: [UploadedFileInput!]!) {
|
|
66
|
+
attachUploadedFilesToMessage(postId: $postId, files: $files) {
|
|
67
|
+
id
|
|
68
|
+
name
|
|
69
|
+
mimeType
|
|
70
|
+
url
|
|
71
|
+
width
|
|
72
|
+
height
|
|
52
73
|
}
|
|
53
74
|
}
|
|
54
75
|
`;
|
|
@@ -67,7 +88,7 @@ const CREATE_DIRECT_CHANNEL_MUTATION = `
|
|
|
67
88
|
const MESSAGE_ADDED_SUBSCRIPTION_WS = `
|
|
68
89
|
subscription ChatMessageAdded_WS($channelId: String) {
|
|
69
90
|
chatMessageAdded(channelId: $channelId) {
|
|
70
|
-
${
|
|
91
|
+
${MESSAGE_FIELDS}
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
`;
|
|
@@ -122,4 +143,4 @@ const YANTRA_CHAT_INVITER_QUERY = `
|
|
|
122
143
|
avatarUrl
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
|
-
`;export{CHANNELS_QUERY,CONTACT_MATCHES_QUERY,CREATE_DIRECT_CHANNEL_MUTATION,MESSAGES_QUERY,MESSAGE_ADDED_SUBSCRIPTION_WS,POST_FIELDS,REACTION_CHANGED_SUBSCRIPTION_WS,REGISTER_YANTRA_CHAT_MEMBER_MUTATION,SEND_MESSAGE_MUTATION,SET_MESSAGE_REACTION_MUTATION,VIEW_CHANNEL_MUTATION,YANTRA_CHAT_INVITER_QUERY,YANTRA_CHAT_REGISTRATION_STATUS_QUERY};//# sourceMappingURL=chatOperations.js.map
|
|
146
|
+
`;export{ATTACH_UPLOADED_FILES_MUTATION,CHANNELS_QUERY,CONTACT_MATCHES_QUERY,CREATE_DIRECT_CHANNEL_MUTATION,CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,MESSAGES_QUERY,MESSAGE_ADDED_SUBSCRIPTION_WS,MESSAGE_FIELDS,POST_FIELDS,REACTION_CHANGED_SUBSCRIPTION_WS,REGISTER_YANTRA_CHAT_MEMBER_MUTATION,SEND_MESSAGE_MUTATION,SET_MESSAGE_REACTION_MUTATION,VIEW_CHANNEL_MUTATION,YANTRA_CHAT_INVITER_QUERY,YANTRA_CHAT_REGISTRATION_STATUS_QUERY};//# sourceMappingURL=chatOperations.js.map
|
|
@@ -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`;\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 }\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 ${POST_FIELDS}\n }\n }\n }\n`;\n\nexport const SEND_MESSAGE_MUTATION = `\n mutation ChatSendMessage($channelId: String!, $messageInput: MessageInput!) {\n sendMessage(channelId: $channelId, messageInput: $messageInput) {\n ${POST_FIELDS}\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 * 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 ${POST_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 SEND_MESSAGE_MUTATION,\n VIEW_CHANNEL_MUTATION,\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;AAgB3B,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAA,EAUH,WAAW,CAAA;AAAA;AAAA;AAAA,CAAA;AAcxB,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGhB,cAAc;AAAA;AAAA;AAAA;AAarB,MAAM,cAAiB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAA,EAKZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAKtB,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA,YAAA,EAGvB,WAAW;AAAA;AAAA;AAAA;AAMlB,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA;AAAA;AAa9B,MAAM,8BAAiC,GAAA;AAAA;AAAA;AAAA,YAAA,EAGhC,cAAc;AAAA;AAAA;AAAA;AAYrB,MAAM,6BAAgC,GAAA;AAAA;AAAA;AAAA,YAAA,EAG/B,WAAW;AAAA;AAAA;AAAA;AASlB,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"}
|
|
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`;\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 }\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/** 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 * 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 SEND_MESSAGE_MUTATION,\n CREATE_MESSAGE_UPLOAD_LINKS_MUTATION,\n ATTACH_UPLOADED_FILES_MUTATION,\n VIEW_CHANNEL_MUTATION,\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;AAqBpB,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,CAAA;AAcxB,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;AAcvC,MAAM,qBAAwB,GAAA;AAAA;AAAA;AAAA;AAAA;AAa9B,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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as React from'react';import {CHANNELS_QUERY,MESSAGE_ADDED_SUBSCRIPTION_WS,CREATE_DIRECT_CHANNEL_MUTATION,MESSAGES_QUERY,SEND_MESSAGE_MUTATION,REACTION_CHANGED_SUBSCRIPTION_WS,SET_MESSAGE_REACTION_MUTATION,VIEW_CHANNEL_MUTATION}from'./chatOperations.js';import {mergeConversations,toConversation,patchConversationAfterPost,sortConversations,mergeMessages,toChatMessage,reconcileOptimistic,applyReactionSummary,withViewerReaction}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,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,VIEW_CHANNEL_MUTATION}from'./chatOperations.js';import {mergeConversations,toConversation,patchConversationAfterPost,sortConversations,mergeMessages,toChatMessage,reconcileOptimistic,ATTACHMENT_LABELS,applyReactionSummary,withViewerReaction}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;
|
|
@@ -17,6 +17,12 @@ var __spreadValues = (a, b) => {
|
|
|
17
17
|
return a;
|
|
18
18
|
};
|
|
19
19
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
function newPostId() {
|
|
21
|
+
const seconds = Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0");
|
|
22
|
+
let rest = "";
|
|
23
|
+
for (let i = 0; i < 16; i += 1) rest += Math.floor(Math.random() * 16).toString(16);
|
|
24
|
+
return seconds + rest;
|
|
25
|
+
}
|
|
20
26
|
const PAGE_SIZE = 20;
|
|
21
27
|
function firstErrorMessage(errors) {
|
|
22
28
|
if (!(errors == null ? void 0 : errors.length)) return null;
|
|
@@ -215,6 +221,7 @@ function useThread(gql, channelId, viewer, opts) {
|
|
|
215
221
|
const viewerId = (_a = viewer == null ? void 0 : viewer.userId) != null ? _a : null;
|
|
216
222
|
const mounted = React.useRef(true);
|
|
217
223
|
const pending = React.useRef(/* @__PURE__ */ new Map());
|
|
224
|
+
const media = opts == null ? void 0 : opts.media;
|
|
218
225
|
const onConversationMessage = opts == null ? void 0 : opts.onConversationMessage;
|
|
219
226
|
const notifyList = React.useCallback((post) => {
|
|
220
227
|
var _a2;
|
|
@@ -310,7 +317,9 @@ function useThread(gql, channelId, viewer, opts) {
|
|
|
310
317
|
reactions: [],
|
|
311
318
|
sendState: "sending"
|
|
312
319
|
};
|
|
313
|
-
pending.current.set(clientId,
|
|
320
|
+
pending.current.set(clientId, {
|
|
321
|
+
body: text
|
|
322
|
+
});
|
|
314
323
|
setMessages((prev) => mergeMessages(prev, [optimistic]));
|
|
315
324
|
onConversationMessage == null ? void 0 : onConversationMessage({
|
|
316
325
|
id: clientId,
|
|
@@ -340,13 +349,122 @@ function useThread(gql, channelId, viewer, opts) {
|
|
|
340
349
|
setMessages((prev) => reconcileOptimistic(prev, toChatMessage(saved, viewerId)));
|
|
341
350
|
notifyList(saved);
|
|
342
351
|
}, [gql, channelId, viewerId, viewer, notifyList, onConversationMessage]);
|
|
352
|
+
const sendAttachments = React.useCallback(async (files, caption, kind) => {
|
|
353
|
+
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
354
|
+
if (!media || !files.length || !channelId || !viewerId) return;
|
|
355
|
+
const text = caption.trim();
|
|
356
|
+
const postId = newPostId();
|
|
357
|
+
const local = files.map((f, i) => {
|
|
358
|
+
var _a3, _b2, _c2;
|
|
359
|
+
return {
|
|
360
|
+
id: `${postId}-${i}`,
|
|
361
|
+
kind,
|
|
362
|
+
name: f.name,
|
|
363
|
+
mimeType: f.mimeType,
|
|
364
|
+
url: f.uri,
|
|
365
|
+
key: null,
|
|
366
|
+
width: (_a3 = f.width) != null ? _a3 : null,
|
|
367
|
+
height: (_b2 = f.height) != null ? _b2 : null,
|
|
368
|
+
durationMs: (_c2 = f.durationMs) != null ? _c2 : null
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
setMessages((prev) => {
|
|
372
|
+
var _a3, _b2;
|
|
373
|
+
return mergeMessages(prev, [{
|
|
374
|
+
id: postId,
|
|
375
|
+
body: text,
|
|
376
|
+
createdAt: Date.now(),
|
|
377
|
+
authorId: viewerId,
|
|
378
|
+
authorName: (_a3 = viewer == null ? void 0 : viewer.displayName) != null ? _a3 : "You",
|
|
379
|
+
authorAvatar: (_b2 = viewer == null ? void 0 : viewer.avatarUrl) != null ? _b2 : null,
|
|
380
|
+
isMine: true,
|
|
381
|
+
isDeleted: false,
|
|
382
|
+
isEdited: false,
|
|
383
|
+
attachments: local,
|
|
384
|
+
reactions: [],
|
|
385
|
+
sendState: "sending"
|
|
386
|
+
}]);
|
|
387
|
+
});
|
|
388
|
+
pending.current.set(postId, {
|
|
389
|
+
body: text,
|
|
390
|
+
files,
|
|
391
|
+
kind
|
|
392
|
+
});
|
|
393
|
+
const fail = (message) => {
|
|
394
|
+
if (!mounted.current) return;
|
|
395
|
+
setMessages((prev) => prev.map((m) => m.id === postId ? __spreadProps(__spreadValues({}, m), {
|
|
396
|
+
sendState: "failed"
|
|
397
|
+
}) : m));
|
|
398
|
+
setError(message);
|
|
399
|
+
};
|
|
400
|
+
try {
|
|
401
|
+
const links = await gql.mutate(CREATE_MESSAGE_UPLOAD_LINKS_MUTATION, {
|
|
402
|
+
postId,
|
|
403
|
+
filenames: files.map((f) => f.name)
|
|
404
|
+
});
|
|
405
|
+
const urls = (_b = (_a2 = links.data) == null ? void 0 : _a2.createMessageFilesUploadLink) != null ? _b : [];
|
|
406
|
+
if (((_c = links.errors) == null ? void 0 : _c.length) || urls.length !== files.length) {
|
|
407
|
+
fail((_d = firstErrorMessage(links.errors)) != null ? _d : "Could not prepare the upload.");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await Promise.all(files.map((f, i) => media.putBytes(urls[i], f.uri, f.mimeType)));
|
|
411
|
+
const attached = await gql.mutate(ATTACH_UPLOADED_FILES_MUTATION, {
|
|
412
|
+
postId,
|
|
413
|
+
files: files.map((f, i) => {
|
|
414
|
+
var _a3, _b2, _c2;
|
|
415
|
+
return {
|
|
416
|
+
name: f.name,
|
|
417
|
+
mimeType: f.mimeType,
|
|
418
|
+
size: (_a3 = f.byteSize) != null ? _a3 : 0,
|
|
419
|
+
width: (_b2 = f.width) != null ? _b2 : null,
|
|
420
|
+
height: (_c2 = f.height) != null ? _c2 : null,
|
|
421
|
+
url: urls[i].split("?")[0]
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
});
|
|
425
|
+
const fileIds = ((_f = (_e = attached.data) == null ? void 0 : _e.attachUploadedFilesToMessage) != null ? _f : []).map((f) => f.id);
|
|
426
|
+
if (((_g = attached.errors) == null ? void 0 : _g.length) || !fileIds.length) {
|
|
427
|
+
fail((_h = firstErrorMessage(attached.errors)) != null ? _h : "Could not attach the upload.");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const durationMs = kind === "audio" ? (_i = files[0]) == null ? void 0 : _i.durationMs : void 0;
|
|
431
|
+
const res = await gql.mutate(SEND_MESSAGE_MUTATION, {
|
|
432
|
+
channelId,
|
|
433
|
+
postId,
|
|
434
|
+
messageInput: {
|
|
435
|
+
// messenger-box rejects blank text (it trims), so a photo / voice note on its
|
|
436
|
+
// own carries its label; the bubble hides it again.
|
|
437
|
+
content: text || ATTACHMENT_LABELS[kind],
|
|
438
|
+
files: fileIds,
|
|
439
|
+
extraProps: __spreadValues({
|
|
440
|
+
attachmentKind: kind
|
|
441
|
+
}, durationMs ? {
|
|
442
|
+
voiceDurationMs: durationMs
|
|
443
|
+
} : {})
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
if (!mounted.current) return;
|
|
447
|
+
const saved = (_j = res.data) == null ? void 0 : _j.sendMessage;
|
|
448
|
+
if (((_k = res.errors) == null ? void 0 : _k.length) || !saved) {
|
|
449
|
+
fail((_l = firstErrorMessage(res.errors)) != null ? _l : "Could not send.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
pending.current.delete(postId);
|
|
453
|
+
setMessages((prev) => reconcileOptimistic(prev, toChatMessage(saved, viewerId)));
|
|
454
|
+
notifyList(saved);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
fail(e instanceof Error ? e.message : "Could not send.");
|
|
457
|
+
}
|
|
458
|
+
}, [media, gql, channelId, viewerId, viewer, notifyList]);
|
|
343
459
|
const retry = React.useCallback((messageId) => {
|
|
344
|
-
|
|
345
|
-
|
|
460
|
+
var _a2;
|
|
461
|
+
const entry = pending.current.get(messageId);
|
|
462
|
+
if (!entry) return;
|
|
346
463
|
pending.current.delete(messageId);
|
|
347
464
|
setMessages((prev) => prev.filter((m) => m.id !== messageId));
|
|
348
|
-
void
|
|
349
|
-
|
|
465
|
+
if (((_a2 = entry.files) == null ? void 0 : _a2.length) && entry.kind) void sendAttachments(entry.files, entry.body, entry.kind);
|
|
466
|
+
else void send(entry.body);
|
|
467
|
+
}, [send, sendAttachments]);
|
|
350
468
|
const loadMore = React.useCallback(() => {
|
|
351
469
|
if (loadingMore || !hasMore || loading) return;
|
|
352
470
|
setLoadingMore(true);
|
|
@@ -405,6 +523,7 @@ function useThread(gql, channelId, viewer, opts) {
|
|
|
405
523
|
hasMore,
|
|
406
524
|
error,
|
|
407
525
|
send,
|
|
526
|
+
sendAttachments: media ? sendAttachments : void 0,
|
|
408
527
|
loadMore,
|
|
409
528
|
retry,
|
|
410
529
|
react
|