turbodesk-livechat-react-native 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (70) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/api/conversation-api.d.ts +9 -0
  3. package/dist/api/conversation-api.d.ts.map +1 -1
  4. package/dist/api/conversation-api.js +15 -1
  5. package/dist/api/conversation-api.js.map +1 -1
  6. package/dist/hooks/use-live-chat.d.ts +1 -0
  7. package/dist/hooks/use-live-chat.d.ts.map +1 -1
  8. package/dist/hooks/use-live-chat.js +2 -0
  9. package/dist/hooks/use-live-chat.js.map +1 -1
  10. package/dist/hooks/use-send-message.d.ts +1 -0
  11. package/dist/hooks/use-send-message.d.ts.map +1 -1
  12. package/dist/hooks/use-send-message.js +20 -14
  13. package/dist/hooks/use-send-message.js.map +1 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/navigation/LiveChatPanel.d.ts +2 -1
  19. package/dist/navigation/LiveChatPanel.d.ts.map +1 -1
  20. package/dist/navigation/LiveChatPanel.js +21 -2
  21. package/dist/navigation/LiveChatPanel.js.map +1 -1
  22. package/dist/navigation/panel-router-context.d.ts +2 -1
  23. package/dist/navigation/panel-router-context.d.ts.map +1 -1
  24. package/dist/navigation/panel-router-context.js +5 -4
  25. package/dist/navigation/panel-router-context.js.map +1 -1
  26. package/dist/provider/LiveChatContext.d.ts.map +1 -1
  27. package/dist/provider/LiveChatContext.js +2 -0
  28. package/dist/provider/LiveChatContext.js.map +1 -1
  29. package/dist/provider/LiveChatProvider.d.ts +1 -1
  30. package/dist/provider/LiveChatProvider.d.ts.map +1 -1
  31. package/dist/provider/LiveChatProvider.js +17 -2
  32. package/dist/provider/LiveChatProvider.js.map +1 -1
  33. package/dist/provider/types.d.ts +4 -0
  34. package/dist/provider/types.d.ts.map +1 -1
  35. package/dist/ui/components/ConversationHeader.d.ts.map +1 -1
  36. package/dist/ui/components/ConversationHeader.js +7 -4
  37. package/dist/ui/components/ConversationHeader.js.map +1 -1
  38. package/dist/ui/components/ConversationListScreen.d.ts.map +1 -1
  39. package/dist/ui/components/ConversationListScreen.js +11 -2
  40. package/dist/ui/components/ConversationListScreen.js.map +1 -1
  41. package/dist/ui/components/ConversationScreen.d.ts.map +1 -1
  42. package/dist/ui/components/ConversationScreen.js +13 -3
  43. package/dist/ui/components/ConversationScreen.js.map +1 -1
  44. package/dist/ui/components/HomeScreen.d.ts.map +1 -1
  45. package/dist/ui/components/HomeScreen.js +14 -4
  46. package/dist/ui/components/HomeScreen.js.map +1 -1
  47. package/dist/ui/components/MessageComposer.d.ts +1 -0
  48. package/dist/ui/components/MessageComposer.d.ts.map +1 -1
  49. package/dist/ui/components/MessageComposer.js +89 -30
  50. package/dist/ui/components/MessageComposer.js.map +1 -1
  51. package/dist/ui/safe-area.d.ts +9 -0
  52. package/dist/ui/safe-area.d.ts.map +1 -0
  53. package/dist/ui/safe-area.js +28 -0
  54. package/dist/ui/safe-area.js.map +1 -0
  55. package/package.json +11 -3
  56. package/src/api/conversation-api.ts +33 -8
  57. package/src/hooks/use-live-chat.ts +3 -0
  58. package/src/hooks/use-send-message.ts +169 -159
  59. package/src/index.ts +3 -0
  60. package/src/navigation/LiveChatPanel.tsx +30 -6
  61. package/src/navigation/panel-router-context.tsx +5 -4
  62. package/src/provider/LiveChatContext.ts +2 -0
  63. package/src/provider/LiveChatProvider.tsx +396 -380
  64. package/src/provider/types.ts +63 -57
  65. package/src/ui/components/ConversationHeader.tsx +7 -6
  66. package/src/ui/components/ConversationListScreen.tsx +18 -5
  67. package/src/ui/components/ConversationScreen.tsx +369 -362
  68. package/src/ui/components/HomeScreen.tsx +21 -6
  69. package/src/ui/components/MessageComposer.tsx +116 -36
  70. package/src/ui/safe-area.ts +34 -0
@@ -1,159 +1,169 @@
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
- }
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
+ clearError: () => void;
32
+ sendText: (
33
+ text: string,
34
+ conversationId?: string | null
35
+ ) => Promise<string | null>;
36
+ sendAttachment: (
37
+ attachment: AttachmentMeta,
38
+ conversationId?: string | null
39
+ ) => Promise<string | null>;
40
+ sendInteractive: (
41
+ reply: InteractiveReplyMeta,
42
+ conversationId?: string | null
43
+ ) => Promise<string | null>;
44
+ };
45
+
46
+ export function useSendMessage(): UseSendMessageResult {
47
+ const { visitorQueryParams } = useLiveChatContext();
48
+ const [sending, setSending] = useState(false);
49
+ const [error, setError] = useState<string | null>(null);
50
+
51
+ const clearError = useCallback(() => setError(null), []);
52
+
53
+ const sendText = useCallback(
54
+ async (text: string, conversationId?: string | null): Promise<string | null> => {
55
+ const trimmed = text.trim();
56
+ if (!trimmed) throw new Error("Message cannot be empty");
57
+ if (!visitorQueryParams) throw new Error("Not connected yet. Please wait and try again.");
58
+
59
+ const localMessageId =
60
+ typeof crypto !== "undefined" && crypto.randomUUID
61
+ ? crypto.randomUUID()
62
+ : "lc-" + Date.now();
63
+ const body: any = {
64
+ localMessageId,
65
+ message: { type: "text", text: { body: trimmed } },
66
+ };
67
+ if (conversationId) body.conversationId = conversationId;
68
+
69
+ setSending(true);
70
+ setError(null);
71
+ try {
72
+ const raw = await conversationApi.sendVisitorMessage(body, {
73
+ params: visitorQueryParams,
74
+ });
75
+ return pickConversationId(raw);
76
+ } catch (e: any) {
77
+ const msg = e?.response?.data?.message ?? e?.message ?? "Failed to send message";
78
+ setError(msg);
79
+ throw new Error(msg);
80
+ } finally {
81
+ setSending(false);
82
+ }
83
+ },
84
+ [visitorQueryParams]
85
+ );
86
+
87
+ const sendAttachment = useCallback(
88
+ async (
89
+ attachment: AttachmentMeta,
90
+ conversationId?: string | null
91
+ ): Promise<string | null> => {
92
+ if (!visitorQueryParams) throw new Error("Not connected yet. Please wait and try again.");
93
+
94
+ const localMessageId =
95
+ typeof crypto !== "undefined" && crypto.randomUUID
96
+ ? crypto.randomUUID()
97
+ : "lc-" + Date.now();
98
+ const typeObj: Record<string, any> = { link: attachment.fileUrl };
99
+ if (attachment.caption) typeObj.caption = attachment.caption;
100
+ if (attachment.type === "document") typeObj.filename = attachment.fileName;
101
+
102
+ const body: any = {
103
+ localMessageId,
104
+ fileId: attachment.fileId,
105
+ fileUrl: attachment.fileUrl,
106
+ fileName: attachment.fileName,
107
+ fileExtension: attachment.fileExtension,
108
+ message: { type: attachment.type, [attachment.type]: typeObj },
109
+ };
110
+ if (conversationId) body.conversationId = conversationId;
111
+
112
+ setSending(true);
113
+ setError(null);
114
+ try {
115
+ const raw = await conversationApi.sendVisitorMessage(body, {
116
+ params: visitorQueryParams,
117
+ });
118
+ return pickConversationId(raw);
119
+ } catch (e: any) {
120
+ const msg = e?.response?.data?.message ?? e?.message ?? "Failed to send attachment";
121
+ setError(msg);
122
+ throw new Error(msg);
123
+ } finally {
124
+ setSending(false);
125
+ }
126
+ },
127
+ [visitorQueryParams]
128
+ );
129
+
130
+ const sendInteractive = useCallback(
131
+ async (reply: InteractiveReplyMeta, conversationId?: string | null): Promise<string | null> => {
132
+ if (!visitorQueryParams) throw new Error("Not connected yet. Please wait and try again.");
133
+
134
+ const localMessageId =
135
+ typeof crypto !== "undefined" && crypto.randomUUID
136
+ ? crypto.randomUUID()
137
+ : "lc-" + Date.now();
138
+ const body: any = {
139
+ localMessageId,
140
+ message: {
141
+ type: "interactive",
142
+ interactive: {
143
+ type: reply.type,
144
+ [reply.type]: { id: reply.id, title: reply.title },
145
+ },
146
+ },
147
+ };
148
+ if (conversationId) body.conversationId = conversationId;
149
+
150
+ setSending(true);
151
+ setError(null);
152
+ try {
153
+ const raw = await conversationApi.sendVisitorMessage(body, {
154
+ params: visitorQueryParams,
155
+ });
156
+ return pickConversationId(raw);
157
+ } catch (e: any) {
158
+ const msg = e?.response?.data?.message ?? e?.message ?? "Failed to send reply";
159
+ setError(msg);
160
+ throw new Error(msg);
161
+ } finally {
162
+ setSending(false);
163
+ }
164
+ },
165
+ [visitorQueryParams]
166
+ );
167
+
168
+ return { sending, error, clearError, sendText, sendAttachment, sendInteractive };
169
+ }
package/src/index.ts CHANGED
@@ -75,6 +75,9 @@ export { PanelRouterProvider, usePanelRouter } from "./navigation/panel-router-c
75
75
  export type { PanelRouterContextValue, WidgetPanelView } from "./navigation/panel-router-context";
76
76
 
77
77
  // ─── Theme ─────────────────────────────────────────────────────────────────────
78
+ export { usePanelSafeAreaInsets } from "./ui/safe-area";
79
+ export type { PanelSafeAreaInsets } from "./ui/safe-area";
80
+
78
81
  export {
79
82
  defaultTheme,
80
83
  mergeTheme,
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useRef } from "react";
2
- import { View, StyleSheet, BackHandler, PanResponder, Animated } from "react-native";
1
+ import React, { useEffect, useRef, type ComponentType, type ReactNode } from "react";
2
+ import { View, StyleSheet, BackHandler, PanResponder } from "react-native";
3
3
  import { PanelRouterProvider, usePanelRouter } from "./panel-router-context";
4
4
  import { HomeScreen } from "../ui/components/HomeScreen";
5
5
  import { ConversationListScreen } from "../ui/components/ConversationListScreen";
@@ -8,10 +8,25 @@ import { useLiveChatContext } from "../provider/LiveChatContext";
8
8
  import { getBrandColor, useEffectiveAppearance } from "../ui/theme";
9
9
  import { WsStatusStrip } from "../ui/components/WsStatusStrip";
10
10
 
11
+ let KeyboardProvider: ComponentType<{ children: ReactNode }> | null = null;
12
+ try {
13
+ KeyboardProvider = require("react-native-keyboard-controller").KeyboardProvider;
14
+ } catch {
15
+ KeyboardProvider = null;
16
+ }
17
+
11
18
  export type LiveChatPanelProps = {
12
19
  onClose?: () => void;
20
+ initialConversationId?: string | null;
13
21
  };
14
22
 
23
+ let PanelSafeAreaProvider: ComponentType<{ children: ReactNode }> | null = null;
24
+ try {
25
+ PanelSafeAreaProvider = require("react-native-safe-area-context").SafeAreaProvider;
26
+ } catch {
27
+ PanelSafeAreaProvider = null;
28
+ }
29
+
15
30
  // Mirrors web PanelShell — neutral chrome for chat screens, brand bg for home
16
31
  function usePanelBackground(view: string): string {
17
32
  const { widgetConfig } = useLiveChatContext();
@@ -27,7 +42,7 @@ function usePanelBackground(view: string): string {
27
42
  const SWIPE_THRESHOLD = 80; // px — minimum horizontal swipe to trigger back
28
43
  const SWIPE_VELOCITY = 0.3; // minimum velocity
29
44
 
30
- function PanelContent({ onClose }: LiveChatPanelProps) {
45
+ function PanelContent({ onClose }: { onClose?: () => void }) {
31
46
  const { view, conversationId, goConversation, goPreviousChats, goBack } = usePanelRouter();
32
47
  const bg = usePanelBackground(view);
33
48
  const canGoBack = view === "conversation" || view === "previousChats";
@@ -103,14 +118,23 @@ function PanelContent({ onClose }: LiveChatPanelProps) {
103
118
  );
104
119
  }
105
120
 
106
- export function LiveChatPanel({ onClose }: LiveChatPanelProps) {
107
- return (
108
- <PanelRouterProvider>
121
+ export function LiveChatPanel({ onClose, initialConversationId }: LiveChatPanelProps) {
122
+ const panel = (
123
+ <PanelRouterProvider initialConversationId={initialConversationId}>
109
124
  <View style={styles.flex}>
110
125
  <PanelContent onClose={onClose} />
111
126
  </View>
112
127
  </PanelRouterProvider>
113
128
  );
129
+
130
+ const withKeyboard = KeyboardProvider ? (
131
+ <KeyboardProvider>{panel}</KeyboardProvider>
132
+ ) : panel;
133
+
134
+ if (PanelSafeAreaProvider) {
135
+ return <PanelSafeAreaProvider>{withKeyboard}</PanelSafeAreaProvider>;
136
+ }
137
+ return withKeyboard;
114
138
  }
115
139
 
116
140
  const styles = StyleSheet.create({
@@ -27,11 +27,12 @@ type RouteEntry = {
27
27
 
28
28
  const PanelRouterContext = createContext<PanelRouterContextValue | null>(null);
29
29
 
30
- export function PanelRouterProvider({ children }: { children: ReactNode }) {
31
- const [view, setView] = useState<WidgetPanelView>("home");
32
- const [conversationId, setConversationId] = useState<string | null>(null);
30
+ export function PanelRouterProvider({ children, initialConversationId }: { children: ReactNode; initialConversationId?: string | null }) {
31
+ const initialView: WidgetPanelView = initialConversationId ? "conversation" : "home";
32
+ const [view, setView] = useState<WidgetPanelView>(initialView);
33
+ const [conversationId, setConversationId] = useState<string | null>(initialConversationId ?? null);
33
34
 
34
- const stack = useRef<RouteEntry[]>([{ view: "home", conversationId: null }]);
35
+ const stack = useRef<RouteEntry[]>([{ view: initialView, conversationId: initialConversationId ?? null }]);
35
36
 
36
37
  const navigate = useCallback((entry: RouteEntry, replace: boolean) => {
37
38
  if (replace) {
@@ -23,6 +23,8 @@ export const LiveChatContext = createContext<LiveChatContextValue>({
23
23
  isVisible: true,
24
24
  show: () => undefined,
25
25
  hide: () => undefined,
26
+ activeConversationId: null,
27
+ setActiveConversationId: () => undefined,
26
28
  wsClient: null,
27
29
  connectionState: { isConnected: false, isConnecting: false, isAwaitingRetry: false, lastError: null },
28
30
  theme: defaultTheme,