turbodesk-livechat-react-native 0.1.0-alpha.0

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 (171) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +91 -0
  3. package/dist/api/conversation-api.d.ts +16 -0
  4. package/dist/api/conversation-api.d.ts.map +1 -0
  5. package/dist/api/conversation-api.js +44 -0
  6. package/dist/api/conversation-api.js.map +1 -0
  7. package/dist/api/file-api.d.ts +5 -0
  8. package/dist/api/file-api.d.ts.map +1 -0
  9. package/dist/api/file-api.js +15 -0
  10. package/dist/api/file-api.js.map +1 -0
  11. package/dist/api/widget-api.d.ts +4 -0
  12. package/dist/api/widget-api.d.ts.map +1 -0
  13. package/dist/api/widget-api.js +15 -0
  14. package/dist/api/widget-api.js.map +1 -0
  15. package/dist/axios/axios.d.ts +32 -0
  16. package/dist/axios/axios.d.ts.map +1 -0
  17. package/dist/axios/axios.js +120 -0
  18. package/dist/axios/axios.js.map +1 -0
  19. package/dist/core/config.d.ts +17 -0
  20. package/dist/core/config.d.ts.map +1 -0
  21. package/dist/core/config.js +42 -0
  22. package/dist/core/config.js.map +1 -0
  23. package/dist/core/http-client.d.ts +33 -0
  24. package/dist/core/http-client.d.ts.map +1 -0
  25. package/dist/core/http-client.js +104 -0
  26. package/dist/core/http-client.js.map +1 -0
  27. package/dist/core/identity.d.ts +7 -0
  28. package/dist/core/identity.d.ts.map +1 -0
  29. package/dist/core/identity.js +62 -0
  30. package/dist/core/identity.js.map +1 -0
  31. package/dist/core/visitor-params.d.ts +15 -0
  32. package/dist/core/visitor-params.d.ts.map +1 -0
  33. package/dist/core/visitor-params.js +45 -0
  34. package/dist/core/visitor-params.js.map +1 -0
  35. package/dist/hooks/use-conversations.d.ts +12 -0
  36. package/dist/hooks/use-conversations.d.ts.map +1 -0
  37. package/dist/hooks/use-conversations.js +177 -0
  38. package/dist/hooks/use-conversations.js.map +1 -0
  39. package/dist/hooks/use-live-chat.d.ts +30 -0
  40. package/dist/hooks/use-live-chat.d.ts.map +1 -0
  41. package/dist/hooks/use-live-chat.js +52 -0
  42. package/dist/hooks/use-live-chat.js.map +1 -0
  43. package/dist/hooks/use-messages.d.ts +11 -0
  44. package/dist/hooks/use-messages.d.ts.map +1 -0
  45. package/dist/hooks/use-messages.js +185 -0
  46. package/dist/hooks/use-messages.js.map +1 -0
  47. package/dist/hooks/use-send-message.d.ts +22 -0
  48. package/dist/hooks/use-send-message.d.ts.map +1 -0
  49. package/dist/hooks/use-send-message.js +125 -0
  50. package/dist/hooks/use-send-message.js.map +1 -0
  51. package/dist/index.d.ts +49 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +97 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/navigation/LiveChatPanel.d.ts +5 -0
  56. package/dist/navigation/LiveChatPanel.d.ts.map +1 -0
  57. package/dist/navigation/LiveChatPanel.js +81 -0
  58. package/dist/navigation/LiveChatPanel.js.map +1 -0
  59. package/dist/navigation/panel-router-context.d.ts +22 -0
  60. package/dist/navigation/panel-router-context.d.ts.map +1 -0
  61. package/dist/navigation/panel-router-context.js +42 -0
  62. package/dist/navigation/panel-router-context.js.map +1 -0
  63. package/dist/navigation/router-types.d.ts +2 -0
  64. package/dist/navigation/router-types.d.ts.map +1 -0
  65. package/dist/navigation/router-types.js +3 -0
  66. package/dist/navigation/router-types.js.map +1 -0
  67. package/dist/provider/LiveChatContext.d.ts +4 -0
  68. package/dist/provider/LiveChatContext.d.ts.map +1 -0
  69. package/dist/provider/LiveChatContext.js +35 -0
  70. package/dist/provider/LiveChatContext.js.map +1 -0
  71. package/dist/provider/LiveChatProvider.d.ts +3 -0
  72. package/dist/provider/LiveChatProvider.d.ts.map +1 -0
  73. package/dist/provider/LiveChatProvider.js +308 -0
  74. package/dist/provider/LiveChatProvider.js.map +1 -0
  75. package/dist/provider/types.d.ts +42 -0
  76. package/dist/provider/types.d.ts.map +1 -0
  77. package/dist/provider/types.js +3 -0
  78. package/dist/provider/types.js.map +1 -0
  79. package/dist/realtime/ws-client.d.ts +51 -0
  80. package/dist/realtime/ws-client.d.ts.map +1 -0
  81. package/dist/realtime/ws-client.js +322 -0
  82. package/dist/realtime/ws-client.js.map +1 -0
  83. package/dist/ui/components/AssigneeAvatar.d.ts +12 -0
  84. package/dist/ui/components/AssigneeAvatar.d.ts.map +1 -0
  85. package/dist/ui/components/AssigneeAvatar.js +58 -0
  86. package/dist/ui/components/AssigneeAvatar.js.map +1 -0
  87. package/dist/ui/components/Avatar.d.ts +10 -0
  88. package/dist/ui/components/Avatar.d.ts.map +1 -0
  89. package/dist/ui/components/Avatar.js +76 -0
  90. package/dist/ui/components/Avatar.js.map +1 -0
  91. package/dist/ui/components/ConversationHeader.d.ts +10 -0
  92. package/dist/ui/components/ConversationHeader.d.ts.map +1 -0
  93. package/dist/ui/components/ConversationHeader.js +90 -0
  94. package/dist/ui/components/ConversationHeader.js.map +1 -0
  95. package/dist/ui/components/ConversationListScreen.d.ts +9 -0
  96. package/dist/ui/components/ConversationListScreen.d.ts.map +1 -0
  97. package/dist/ui/components/ConversationListScreen.js +350 -0
  98. package/dist/ui/components/ConversationListScreen.js.map +1 -0
  99. package/dist/ui/components/ConversationScreen.d.ts +8 -0
  100. package/dist/ui/components/ConversationScreen.d.ts.map +1 -0
  101. package/dist/ui/components/ConversationScreen.js +235 -0
  102. package/dist/ui/components/ConversationScreen.js.map +1 -0
  103. package/dist/ui/components/HomeScreen.d.ts +6 -0
  104. package/dist/ui/components/HomeScreen.d.ts.map +1 -0
  105. package/dist/ui/components/HomeScreen.js +133 -0
  106. package/dist/ui/components/HomeScreen.js.map +1 -0
  107. package/dist/ui/components/LivechatMessageRenderer.d.ts +17 -0
  108. package/dist/ui/components/LivechatMessageRenderer.d.ts.map +1 -0
  109. package/dist/ui/components/LivechatMessageRenderer.js +122 -0
  110. package/dist/ui/components/LivechatMessageRenderer.js.map +1 -0
  111. package/dist/ui/components/LogMessage.d.ts +5 -0
  112. package/dist/ui/components/LogMessage.d.ts.map +1 -0
  113. package/dist/ui/components/LogMessage.js +83 -0
  114. package/dist/ui/components/LogMessage.js.map +1 -0
  115. package/dist/ui/components/MessageBubble.d.ts +15 -0
  116. package/dist/ui/components/MessageBubble.d.ts.map +1 -0
  117. package/dist/ui/components/MessageBubble.js +84 -0
  118. package/dist/ui/components/MessageBubble.js.map +1 -0
  119. package/dist/ui/components/MessageComposer.d.ts +31 -0
  120. package/dist/ui/components/MessageComposer.d.ts.map +1 -0
  121. package/dist/ui/components/MessageComposer.js +295 -0
  122. package/dist/ui/components/MessageComposer.js.map +1 -0
  123. package/dist/ui/components/WsStatusStrip.d.ts +2 -0
  124. package/dist/ui/components/WsStatusStrip.d.ts.map +1 -0
  125. package/dist/ui/components/WsStatusStrip.js +103 -0
  126. package/dist/ui/components/WsStatusStrip.js.map +1 -0
  127. package/dist/ui/icons.d.ts +22 -0
  128. package/dist/ui/icons.d.ts.map +1 -0
  129. package/dist/ui/icons.js +71 -0
  130. package/dist/ui/icons.js.map +1 -0
  131. package/dist/ui/theme.d.ts +72 -0
  132. package/dist/ui/theme.d.ts.map +1 -0
  133. package/dist/ui/theme.js +170 -0
  134. package/dist/ui/theme.js.map +1 -0
  135. package/docs/backend-contract.md +392 -0
  136. package/docs/migration-notes.md +32 -0
  137. package/package.json +60 -0
  138. package/src/api/conversation-api.ts +71 -0
  139. package/src/api/file-api.ts +14 -0
  140. package/src/api/widget-api.ts +12 -0
  141. package/src/axios/axios.ts +159 -0
  142. package/src/core/config.ts +54 -0
  143. package/src/core/http-client.ts +136 -0
  144. package/src/core/identity.ts +68 -0
  145. package/src/core/visitor-params.ts +48 -0
  146. package/src/hooks/use-conversations.ts +181 -0
  147. package/src/hooks/use-live-chat.ts +84 -0
  148. package/src/hooks/use-messages.ts +188 -0
  149. package/src/hooks/use-send-message.ts +159 -0
  150. package/src/index.ts +114 -0
  151. package/src/navigation/LiveChatPanel.tsx +118 -0
  152. package/src/navigation/panel-router-context.tsx +89 -0
  153. package/src/navigation/router-types.ts +1 -0
  154. package/src/provider/LiveChatContext.ts +33 -0
  155. package/src/provider/LiveChatProvider.tsx +380 -0
  156. package/src/provider/types.ts +57 -0
  157. package/src/realtime/ws-client.ts +369 -0
  158. package/src/types/react-native-svg.d.ts +10 -0
  159. package/src/ui/components/AssigneeAvatar.tsx +102 -0
  160. package/src/ui/components/Avatar.tsx +110 -0
  161. package/src/ui/components/ConversationHeader.tsx +202 -0
  162. package/src/ui/components/ConversationListScreen.tsx +454 -0
  163. package/src/ui/components/ConversationScreen.tsx +362 -0
  164. package/src/ui/components/HomeScreen.tsx +278 -0
  165. package/src/ui/components/LivechatMessageRenderer.tsx +268 -0
  166. package/src/ui/components/LogMessage.tsx +88 -0
  167. package/src/ui/components/MessageBubble.tsx +148 -0
  168. package/src/ui/components/MessageComposer.tsx +461 -0
  169. package/src/ui/components/WsStatusStrip.tsx +123 -0
  170. package/src/ui/icons.tsx +111 -0
  171. package/src/ui/theme.ts +237 -0
@@ -0,0 +1,84 @@
1
+ import { useMemo } from "react";
2
+ import { useLiveChatContext } from "../provider/LiveChatContext";
3
+ import type { WsConnectionState } from "../realtime/ws-client";
4
+ import type { ThemeConfig } from "../ui/theme";
5
+
6
+ export type LiveChatState = {
7
+ isOpen: boolean;
8
+ isVisible: boolean;
9
+ widgetId: string;
10
+ userId: string;
11
+ userToken: string | undefined;
12
+ config: any;
13
+ embedLoadState: "loading" | "ready" | "error";
14
+ embedLoadError: string | null;
15
+ totalUnread: number;
16
+ widgetName: string;
17
+ widgetGreeting: string;
18
+ connectionState: WsConnectionState;
19
+ theme: ThemeConfig;
20
+ };
21
+
22
+ export type LiveChatActions = {
23
+ open: () => void;
24
+ close: () => void;
25
+ show: () => void;
26
+ hide: () => void;
27
+ setToken: (token: string | null | undefined) => void;
28
+ logout: () => void;
29
+ };
30
+
31
+ export type UseLiveChatResult = {
32
+ state: LiveChatState;
33
+ } & LiveChatActions;
34
+
35
+ export function useLiveChat(): UseLiveChatResult {
36
+ const ctx = useLiveChatContext();
37
+
38
+ return useMemo(
39
+ () => ({
40
+ state: {
41
+ isOpen: ctx.isOpen,
42
+ isVisible: ctx.isVisible,
43
+ widgetId: ctx.widgetId,
44
+ userId: ctx.userId,
45
+ userToken: ctx.userToken,
46
+ config: ctx.widgetConfig,
47
+ embedLoadState: ctx.embedLoadState,
48
+ embedLoadError: ctx.embedLoadError,
49
+ totalUnread: ctx.totalUnread,
50
+ widgetName: ctx.widgetName,
51
+ widgetGreeting: ctx.widgetGreeting,
52
+ connectionState: ctx.connectionState,
53
+ theme: ctx.theme,
54
+ },
55
+ open: ctx.open,
56
+ close: ctx.close,
57
+ show: ctx.show,
58
+ hide: ctx.hide,
59
+ setToken: ctx.setToken,
60
+ logout: ctx.logout,
61
+ }),
62
+ [
63
+ ctx.isOpen,
64
+ ctx.isVisible,
65
+ ctx.widgetId,
66
+ ctx.userId,
67
+ ctx.userToken,
68
+ ctx.widgetConfig,
69
+ ctx.embedLoadState,
70
+ ctx.embedLoadError,
71
+ ctx.totalUnread,
72
+ ctx.widgetName,
73
+ ctx.widgetGreeting,
74
+ ctx.connectionState,
75
+ ctx.theme,
76
+ ctx.open,
77
+ ctx.close,
78
+ ctx.show,
79
+ ctx.hide,
80
+ ctx.setToken,
81
+ ctx.logout,
82
+ ]
83
+ );
84
+ }
@@ -0,0 +1,188 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { conversationApi } from "../api/conversation-api";
3
+ import { useLiveChatContext } from "../provider/LiveChatContext";
4
+
5
+ const DEFAULT_LIMIT = 30;
6
+
7
+ function unwrapMessages(raw: any): { messages: any[]; nextCursor: string | null } {
8
+ if (!raw) return { messages: [], nextCursor: null };
9
+ const a = raw?.data !== undefined ? raw.data : raw;
10
+ const b = a?.data !== undefined && !Array.isArray(a) ? a.data : a;
11
+ const messages: any[] = b?.messages ?? (Array.isArray(b) ? b : []);
12
+ const nextCursor: string | null = b?.pagination?.nextCursor ?? null;
13
+ return { messages, nextCursor };
14
+ }
15
+
16
+ export type UseMessagesResult = {
17
+ messages: any[];
18
+ loading: boolean;
19
+ loadingOlder: boolean;
20
+ hasOlder: boolean;
21
+ error: string | null;
22
+ loadOlder: () => void;
23
+ markAsRead: () => void;
24
+ };
25
+
26
+ export function useMessages(conversationId: string | null | undefined): UseMessagesResult {
27
+ const { wsClient, visitorQueryParams, setTotalUnread } = useLiveChatContext();
28
+
29
+ const [messages, setMessages] = useState<any[]>([]);
30
+ const [loading, setLoading] = useState(false);
31
+ const [loadingOlder, setLoadingOlder] = useState(false);
32
+ const [hasOlder, setHasOlder] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const nextCursorRef = useRef<string | null>(null);
35
+ const messagesRef = useRef<any[]>([]);
36
+ messagesRef.current = messages;
37
+ const loadOlderInFlightRef = useRef(false);
38
+ const prevConvIdRef = useRef<string | null | undefined>(null);
39
+
40
+ useEffect(() => {
41
+ const isNew = !conversationId || conversationId === "new";
42
+ if (isNew || !visitorQueryParams) {
43
+ if (!conversationId) {
44
+ setMessages([]);
45
+ setLoading(false);
46
+ setError(null);
47
+ setHasOlder(false);
48
+ nextCursorRef.current = null;
49
+ }
50
+ prevConvIdRef.current = conversationId;
51
+ return;
52
+ }
53
+ const wasNew = !prevConvIdRef.current || prevConvIdRef.current === "new";
54
+ if (prevConvIdRef.current !== conversationId) {
55
+ if (!wasNew) {
56
+ setMessages([]);
57
+ nextCursorRef.current = null;
58
+ }
59
+ }
60
+ prevConvIdRef.current = conversationId;
61
+
62
+ const ac = new AbortController();
63
+ // Skip fetch entirely when transitioning from "new" and we already have messages from WS
64
+ if (wasNew && messagesRef.current.length > 0) {
65
+ return () => ac.abort();
66
+ }
67
+ setLoading(true);
68
+ setError(null);
69
+
70
+ (async () => {
71
+ try {
72
+ const raw = await conversationApi.getMessages(conversationId, {
73
+ params: { ...visitorQueryParams, limit: DEFAULT_LIMIT },
74
+ signal: ac.signal,
75
+ });
76
+ if (ac.signal.aborted) return;
77
+ const { messages: msgs, nextCursor } = unwrapMessages(raw);
78
+ // Merge with existing messages (e.g. WS messages already appended during "new" state)
79
+ setMessages((prev) => {
80
+ if (prev.length === 0) return msgs;
81
+ const merged = [...msgs];
82
+ for (const m of prev) {
83
+ if (!merged.some((x) => x._id === m._id || (m.localMessageId && x.localMessageId === m.localMessageId))) {
84
+ merged.push(m);
85
+ }
86
+ }
87
+ return merged.sort((a, b) => {
88
+ const ta = new Date(a?.createdAt || a?.livechat?.timestamp || 0).getTime();
89
+ const tb = new Date(b?.createdAt || b?.livechat?.timestamp || 0).getTime();
90
+ return ta - tb;
91
+ });
92
+ });
93
+ nextCursorRef.current = nextCursor;
94
+ setHasOlder(nextCursor !== null);
95
+ } catch (e: any) {
96
+ if (ac.signal.aborted || e?.name === "AbortError") return;
97
+ setError("Could not load messages.");
98
+ } finally {
99
+ if (!ac.signal.aborted) setLoading(false);
100
+ }
101
+ })();
102
+
103
+ return () => ac.abort();
104
+ }, [conversationId, visitorQueryParams]);
105
+
106
+ const loadOlder = useCallback(async () => {
107
+ if (
108
+ loadOlderInFlightRef.current ||
109
+ !nextCursorRef.current ||
110
+ !conversationId ||
111
+ !visitorQueryParams
112
+ )
113
+ return;
114
+ loadOlderInFlightRef.current = true;
115
+ setLoadingOlder(true);
116
+ try {
117
+ const raw = await conversationApi.getMessages(conversationId, {
118
+ params: {
119
+ ...visitorQueryParams,
120
+ limit: DEFAULT_LIMIT,
121
+ nextCursor: nextCursorRef.current,
122
+ },
123
+ });
124
+ const { messages: older, nextCursor } = unwrapMessages(raw);
125
+ setMessages((prev) => [...older, ...prev]);
126
+ nextCursorRef.current = nextCursor;
127
+ setHasOlder(nextCursor !== null);
128
+ } catch {
129
+ /* ignore */
130
+ } finally {
131
+ loadOlderInFlightRef.current = false;
132
+ setLoadingOlder(false);
133
+ }
134
+ }, [conversationId, visitorQueryParams]);
135
+
136
+ const markAsRead = useCallback(() => {
137
+ if (!conversationId || conversationId === "new" || !visitorQueryParams) return;
138
+ conversationApi
139
+ .markMessageAsRead(conversationId, { params: visitorQueryParams })
140
+ .catch(() => { /* best-effort */ });
141
+ }, [conversationId, visitorQueryParams]);
142
+
143
+ // ── WS event handlers ──────────────────────────────────────────────────────
144
+ useEffect(() => {
145
+ if (!wsClient || !conversationId) return;
146
+
147
+ const handleNewMessage = (payload: any) => {
148
+ const data = payload?.data ?? payload;
149
+ const cid = data?.conversation?._id;
150
+ // When conversationId is "new", accept messages for any conversation (first message)
151
+ if (conversationId !== "new" && cid !== conversationId) return;
152
+ const incoming = data?.message;
153
+ if (!incoming) return;
154
+ setMessages((prev) => {
155
+ if (
156
+ incoming.localMessageId &&
157
+ prev.some((m) => m?.localMessageId === incoming.localMessageId)
158
+ )
159
+ return prev;
160
+ if (incoming._id && prev.some((m) => m?._id === incoming._id)) return prev;
161
+ const next = [...prev, incoming];
162
+ return next.sort((a, b) => {
163
+ const ta = new Date(a?.createdAt || a?.livechat?.timestamp || 0).getTime();
164
+ const tb = new Date(b?.createdAt || b?.livechat?.timestamp || 0).getTime();
165
+ return ta - tb;
166
+ });
167
+ });
168
+ };
169
+
170
+ const handleNewLogMessage = (payload: any) => {
171
+ const data = payload?.data ?? payload;
172
+ const cid = data?.conversation?._id;
173
+ if (conversationId !== "new" && cid !== conversationId) return;
174
+ const incoming = data?.message;
175
+ if (!incoming) return;
176
+ setMessages((prev) => {
177
+ if (incoming._id && prev.some((m) => m?._id === incoming._id)) return prev;
178
+ return [...prev, incoming];
179
+ });
180
+ };
181
+
182
+ const u1 = wsClient.subscribe("new_livechat_message", handleNewMessage);
183
+ const u2 = wsClient.subscribe("new_log_message", handleNewLogMessage);
184
+ return () => { u1(); u2(); };
185
+ }, [wsClient, conversationId]);
186
+
187
+ return { messages, loading, loadingOlder, hasOlder, error, loadOlder, markAsRead };
188
+ }
@@ -0,0 +1,159 @@
1
+ import { useCallback, useState } from "react";
2
+ import { conversationApi } from "../api/conversation-api";
3
+ import { useLiveChatContext } from "../provider/LiveChatContext";
4
+
5
+ function pickConversationId(raw: any): string | null {
6
+ if (!raw) return null;
7
+ const a = raw?.data !== undefined ? raw.data : raw;
8
+ const b = a?.data !== undefined ? a.data : a;
9
+ const id = b?.conversationId ?? a?.conversationId;
10
+ return id != null && id !== "" ? String(id) : null;
11
+ }
12
+
13
+ export type AttachmentMeta = {
14
+ fileId?: string;
15
+ fileUrl: string;
16
+ fileName: string;
17
+ fileExtension?: string;
18
+ type: "image" | "video" | "document" | "audio";
19
+ caption?: string;
20
+ };
21
+
22
+ export type InteractiveReplyMeta = {
23
+ type: "button_reply" | "list_reply";
24
+ id: string;
25
+ title: string;
26
+ };
27
+
28
+ export type UseSendMessageResult = {
29
+ sending: boolean;
30
+ error: string | null;
31
+ sendText: (
32
+ text: string,
33
+ conversationId?: string | null
34
+ ) => Promise<string | null>;
35
+ sendAttachment: (
36
+ attachment: AttachmentMeta,
37
+ conversationId?: string | null
38
+ ) => Promise<string | null>;
39
+ sendInteractive: (
40
+ reply: InteractiveReplyMeta,
41
+ conversationId?: string | null
42
+ ) => Promise<string | null>;
43
+ };
44
+
45
+ export function useSendMessage(): UseSendMessageResult {
46
+ const { visitorQueryParams } = useLiveChatContext();
47
+ const [sending, setSending] = useState(false);
48
+ const [error, setError] = useState<string | null>(null);
49
+
50
+ const sendText = useCallback(
51
+ async (text: string, conversationId?: string | null): Promise<string | null> => {
52
+ const trimmed = text.trim();
53
+ if (!trimmed || !visitorQueryParams) return null;
54
+ const localMessageId =
55
+ typeof crypto !== "undefined" && crypto.randomUUID
56
+ ? crypto.randomUUID()
57
+ : "lc-" + Date.now();
58
+ const body: any = {
59
+ localMessageId,
60
+ message: { type: "text", text: { body: trimmed } },
61
+ };
62
+ if (conversationId) body.conversationId = conversationId;
63
+
64
+ setSending(true);
65
+ setError(null);
66
+ try {
67
+ const raw = await conversationApi.sendVisitorMessage(body, {
68
+ params: visitorQueryParams,
69
+ });
70
+ return pickConversationId(raw);
71
+ } catch (e: any) {
72
+ setError(e?.message ?? "Failed to send message");
73
+ return null;
74
+ } finally {
75
+ setSending(false);
76
+ }
77
+ },
78
+ [visitorQueryParams]
79
+ );
80
+
81
+ const sendAttachment = useCallback(
82
+ async (
83
+ attachment: AttachmentMeta,
84
+ conversationId?: string | null
85
+ ): Promise<string | null> => {
86
+ if (!visitorQueryParams) return null;
87
+ const localMessageId =
88
+ typeof crypto !== "undefined" && crypto.randomUUID
89
+ ? crypto.randomUUID()
90
+ : "lc-" + Date.now();
91
+ const typeObj: Record<string, any> = { link: attachment.fileUrl };
92
+ if (attachment.caption) typeObj.caption = attachment.caption;
93
+ if (attachment.type === "document") typeObj.filename = attachment.fileName;
94
+
95
+ const body: any = {
96
+ localMessageId,
97
+ fileId: attachment.fileId,
98
+ fileUrl: attachment.fileUrl,
99
+ fileName: attachment.fileName,
100
+ fileExtension: attachment.fileExtension,
101
+ message: { type: attachment.type, [attachment.type]: typeObj },
102
+ };
103
+ if (conversationId) body.conversationId = conversationId;
104
+
105
+ setSending(true);
106
+ setError(null);
107
+ try {
108
+ const raw = await conversationApi.sendVisitorMessage(body, {
109
+ params: visitorQueryParams,
110
+ });
111
+ return pickConversationId(raw);
112
+ } catch (e: any) {
113
+ setError(e?.message ?? "Failed to send attachment");
114
+ return null;
115
+ } finally {
116
+ setSending(false);
117
+ }
118
+ },
119
+ [visitorQueryParams]
120
+ );
121
+
122
+ const sendInteractive = useCallback(
123
+ async (reply: InteractiveReplyMeta, conversationId?: string | null): Promise<string | null> => {
124
+ if (!visitorQueryParams) return null;
125
+ const localMessageId =
126
+ typeof crypto !== "undefined" && crypto.randomUUID
127
+ ? crypto.randomUUID()
128
+ : "lc-" + Date.now();
129
+ const body: any = {
130
+ localMessageId,
131
+ message: {
132
+ type: "interactive",
133
+ interactive: {
134
+ type: reply.type,
135
+ [reply.type]: { id: reply.id, title: reply.title },
136
+ },
137
+ },
138
+ };
139
+ if (conversationId) body.conversationId = conversationId;
140
+
141
+ setSending(true);
142
+ setError(null);
143
+ try {
144
+ const raw = await conversationApi.sendVisitorMessage(body, {
145
+ params: visitorQueryParams,
146
+ });
147
+ return pickConversationId(raw);
148
+ } catch (e: any) {
149
+ setError(e?.message ?? "Failed to send reply");
150
+ return null;
151
+ } finally {
152
+ setSending(false);
153
+ }
154
+ },
155
+ [visitorQueryParams]
156
+ );
157
+
158
+ return { sending, error, sendText, sendAttachment, sendInteractive };
159
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ // ─── Provider ─────────────────────────────────────────────────────────────────
2
+ export { LiveChatProvider } from "./provider/LiveChatProvider";
3
+ export { LiveChatContext, useLiveChatContext } from "./provider/LiveChatContext";
4
+ export type { LiveChatProviderProps, LiveChatContextValue, EmbedConfigLoadState } from "./provider/types";
5
+
6
+ // ─── Hooks ─────────────────────────────────────────────────────────────────────
7
+ export { useLiveChat } from "./hooks/use-live-chat";
8
+ export type { LiveChatState, LiveChatActions, UseLiveChatResult } from "./hooks/use-live-chat";
9
+
10
+ export { useConversations } from "./hooks/use-conversations";
11
+ export type { UseConversationsResult } from "./hooks/use-conversations";
12
+
13
+ export { useMessages } from "./hooks/use-messages";
14
+ export type { UseMessagesResult } from "./hooks/use-messages";
15
+
16
+ export { useSendMessage } from "./hooks/use-send-message";
17
+ export type { UseSendMessageResult, AttachmentMeta } from "./hooks/use-send-message";
18
+
19
+ // ─── UI Components ─────────────────────────────────────────────────────────────
20
+ export { ConversationListScreen, formatListMessageDate } from "./ui/components/ConversationListScreen";
21
+ export type { ConversationListScreenProps } from "./ui/components/ConversationListScreen";
22
+
23
+ export { ConversationScreen } from "./ui/components/ConversationScreen";
24
+ export type { ConversationScreenProps } from "./ui/components/ConversationScreen";
25
+
26
+ export { ConversationHeader } from "./ui/components/ConversationHeader";
27
+ export type { ConversationHeaderProps } from "./ui/components/ConversationHeader";
28
+
29
+ export { MessageBubble } from "./ui/components/MessageBubble";
30
+ export type { MessageBubbleProps, BubblePosition } from "./ui/components/MessageBubble";
31
+
32
+ export { LivechatMessageRenderer } from "./ui/components/LivechatMessageRenderer";
33
+ export type { LivechatMessageRendererProps, InteractiveReplyPayload, ShowListPickerFn } from "./ui/components/LivechatMessageRenderer";
34
+
35
+ export { LogMessage } from "./ui/components/LogMessage";
36
+ export type { LogMessageProps } from "./ui/components/LogMessage";
37
+
38
+ export { MessageComposer } from "./ui/components/MessageComposer";
39
+ export type { MessageComposerProps, PickedAttachment, PickedAttachmentKind, UploadedAttachment, PendingAttachment } from "./ui/components/MessageComposer";
40
+
41
+ export { Avatar } from "./ui/components/Avatar";
42
+ export type { AvatarProps } from "./ui/components/Avatar";
43
+
44
+ export {
45
+ ArrowRightIcon,
46
+ ArrowRightCircleIcon,
47
+ ArrowLeftIcon,
48
+ ChevronDownIcon,
49
+ ChevronLeftIcon,
50
+ CloseIcon,
51
+ ChatBubbleIcon,
52
+ MinimizeIcon,
53
+ DotsIcon,
54
+ HistoryChatsIcon,
55
+ SendPlaneIcon,
56
+ PaperclipIcon,
57
+ DownloadIcon,
58
+ PlayIcon,
59
+ FileIcon,
60
+ TurbodeskLogoIcon,
61
+ } from "./ui/icons";
62
+ export type { IconProps } from "./ui/icons";
63
+
64
+ export { AssigneeAvatar } from "./ui/components/AssigneeAvatar";
65
+ export type { AssigneeAvatarProps, AssigneeType } from "./ui/components/AssigneeAvatar";
66
+
67
+ export { HomeScreen } from "./ui/components/HomeScreen";
68
+ export type { HomeScreenProps } from "./ui/components/HomeScreen";
69
+
70
+ // ─── Navigation ──────────────────────────────────────────────────────────────
71
+ export { LiveChatPanel } from "./navigation/LiveChatPanel";
72
+ export type { LiveChatPanelProps } from "./navigation/LiveChatPanel";
73
+
74
+ export { PanelRouterProvider, usePanelRouter } from "./navigation/panel-router-context";
75
+ export type { PanelRouterContextValue, WidgetPanelView } from "./navigation/panel-router-context";
76
+
77
+ // ─── Theme ─────────────────────────────────────────────────────────────────────
78
+ export {
79
+ defaultTheme,
80
+ mergeTheme,
81
+ buildThemeFromBrand,
82
+ contrastingTextOnBrand,
83
+ getBrandColor,
84
+ getNotificationIconColor,
85
+ getWidgetName,
86
+ getWidgetGreeting,
87
+ getWidgetTitle,
88
+ getWidgetDescription,
89
+ getBrandLogoUrl,
90
+ useEffectiveAppearance,
91
+ POWERED_BY_TEXT,
92
+ } from "./ui/theme";
93
+ export type {
94
+ ThemeConfig,
95
+ ThemeColors,
96
+ ThemeFontSizes,
97
+ ThemeSpacing,
98
+ ThemeBorderRadius,
99
+ AppearanceMode,
100
+ } from "./ui/theme";
101
+
102
+ // ─── Low-level (advanced usage) ────────────────────────────────────────────────
103
+ export { WsClient } from "./realtime/ws-client";
104
+ export type { WsConnectionState } from "./realtime/ws-client";
105
+
106
+ export { default as getApiClient } from "./axios/axios";
107
+ export type { RequestOptions, ApiClient } from "./axios/axios";
108
+
109
+ export { widgetApi } from "./api/widget-api";
110
+ export { conversationApi } from "./api/conversation-api";
111
+ export { fileApi } from "./api/file-api";
112
+
113
+ export { buildVisitorQueryParams, getUserPresenceStatus } from "./core/visitor-params";
114
+ export type { VisitorQueryParams, VisitorProfile } from "./core/visitor-params";
@@ -0,0 +1,118 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { View, StyleSheet, BackHandler, PanResponder, Animated } from "react-native";
3
+ import { PanelRouterProvider, usePanelRouter } from "./panel-router-context";
4
+ import { HomeScreen } from "../ui/components/HomeScreen";
5
+ import { ConversationListScreen } from "../ui/components/ConversationListScreen";
6
+ import { ConversationScreen } from "../ui/components/ConversationScreen";
7
+ import { useLiveChatContext } from "../provider/LiveChatContext";
8
+ import { getBrandColor, useEffectiveAppearance } from "../ui/theme";
9
+ import { WsStatusStrip } from "../ui/components/WsStatusStrip";
10
+
11
+ export type LiveChatPanelProps = {
12
+ onClose?: () => void;
13
+ };
14
+
15
+ // Mirrors web PanelShell — neutral chrome for chat screens, brand bg for home
16
+ function usePanelBackground(view: string): string {
17
+ const { widgetConfig } = useLiveChatContext();
18
+ const settings = widgetConfig?.widgetSettings ?? {};
19
+ const appearance = useEffectiveAppearance(settings);
20
+ const customBg = settings?.backgroundColor?.trim();
21
+ if (customBg) return customBg;
22
+ const useNeutralChrome = view === "conversation" || view === "previousChats";
23
+ if (useNeutralChrome) return appearance === "dark" ? "#111113" : "#f9fafb";
24
+ return getBrandColor(settings);
25
+ }
26
+
27
+ const SWIPE_THRESHOLD = 80; // px — minimum horizontal swipe to trigger back
28
+ const SWIPE_VELOCITY = 0.3; // minimum velocity
29
+
30
+ function PanelContent({ onClose }: LiveChatPanelProps) {
31
+ const { view, conversationId, goConversation, goPreviousChats, goBack } = usePanelRouter();
32
+ const bg = usePanelBackground(view);
33
+ const canGoBack = view === "conversation" || view === "previousChats";
34
+
35
+ // Android hardware back button
36
+ useEffect(() => {
37
+ const handler = BackHandler.addEventListener("hardwareBackPress", () => {
38
+ if (canGoBack) { goBack(); return true; }
39
+ return false;
40
+ });
41
+ return () => handler.remove();
42
+ }, [canGoBack, goBack]);
43
+
44
+ // Swipe-back gesture (right swipe from left edge only)
45
+ const panResponder = useRef(
46
+ PanResponder.create({
47
+ onMoveShouldSetPanResponder: (evt, gs) => {
48
+ const x = evt.nativeEvent.pageX;
49
+ return x < 40 && gs.dx > 10 && Math.abs(gs.dy) < Math.abs(gs.dx);
50
+ },
51
+ onPanResponderRelease: (_, gs) => {
52
+ if (gs.dx > SWIPE_THRESHOLD && gs.vx > SWIPE_VELOCITY) {
53
+ goBackRef.current?.();
54
+ }
55
+ },
56
+ })
57
+ ).current;
58
+
59
+ // ref so panResponder closure always has latest goBack + canGoBack
60
+ const goBackRef = useRef<(() => void) | null>(null);
61
+ goBackRef.current = canGoBack ? goBack : null;
62
+
63
+ const screenContent = (() => {
64
+ if (view === "home") {
65
+ return (
66
+ <HomeScreen
67
+ onStartConversation={() => goConversation()}
68
+ onOpenPreviousChats={() => goPreviousChats()}
69
+ />
70
+ );
71
+ }
72
+ if (view === "previousChats") {
73
+ return (
74
+ <ConversationListScreen
75
+ onSelectConversation={(id) => goConversation(id)}
76
+ onNewConversation={() => goConversation()}
77
+ onBack={goBack}
78
+ onClose={onClose}
79
+ />
80
+ );
81
+ }
82
+ if (view === "conversation") {
83
+ return (
84
+ <ConversationScreen
85
+ conversationId={conversationId}
86
+ onBack={goBack}
87
+ onClose={onClose}
88
+ onConversationCreated={(id) => goConversation(id, { replace: true })}
89
+ />
90
+ );
91
+ }
92
+ return null;
93
+ })();
94
+
95
+ return (
96
+ <View
97
+ style={[styles.flex, { backgroundColor: bg }]}
98
+ {...(canGoBack ? panResponder.panHandlers : {})}
99
+ >
100
+ <View style={styles.flex}>{screenContent}</View>
101
+ <WsStatusStrip />
102
+ </View>
103
+ );
104
+ }
105
+
106
+ export function LiveChatPanel({ onClose }: LiveChatPanelProps) {
107
+ return (
108
+ <PanelRouterProvider>
109
+ <View style={styles.flex}>
110
+ <PanelContent onClose={onClose} />
111
+ </View>
112
+ </PanelRouterProvider>
113
+ );
114
+ }
115
+
116
+ const styles = StyleSheet.create({
117
+ flex: { flex: 1 },
118
+ });