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

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 (64) 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.map +1 -1
  19. package/dist/navigation/LiveChatPanel.js +20 -1
  20. package/dist/navigation/LiveChatPanel.js.map +1 -1
  21. package/dist/provider/LiveChatContext.d.ts.map +1 -1
  22. package/dist/provider/LiveChatContext.js +2 -0
  23. package/dist/provider/LiveChatContext.js.map +1 -1
  24. package/dist/provider/LiveChatProvider.d.ts +1 -1
  25. package/dist/provider/LiveChatProvider.d.ts.map +1 -1
  26. package/dist/provider/LiveChatProvider.js +17 -2
  27. package/dist/provider/LiveChatProvider.js.map +1 -1
  28. package/dist/provider/types.d.ts +4 -0
  29. package/dist/provider/types.d.ts.map +1 -1
  30. package/dist/ui/components/ConversationHeader.d.ts.map +1 -1
  31. package/dist/ui/components/ConversationHeader.js +7 -4
  32. package/dist/ui/components/ConversationHeader.js.map +1 -1
  33. package/dist/ui/components/ConversationListScreen.d.ts.map +1 -1
  34. package/dist/ui/components/ConversationListScreen.js +11 -2
  35. package/dist/ui/components/ConversationListScreen.js.map +1 -1
  36. package/dist/ui/components/ConversationScreen.d.ts.map +1 -1
  37. package/dist/ui/components/ConversationScreen.js +13 -3
  38. package/dist/ui/components/ConversationScreen.js.map +1 -1
  39. package/dist/ui/components/HomeScreen.d.ts.map +1 -1
  40. package/dist/ui/components/HomeScreen.js +14 -4
  41. package/dist/ui/components/HomeScreen.js.map +1 -1
  42. package/dist/ui/components/MessageComposer.d.ts +1 -0
  43. package/dist/ui/components/MessageComposer.d.ts.map +1 -1
  44. package/dist/ui/components/MessageComposer.js +89 -30
  45. package/dist/ui/components/MessageComposer.js.map +1 -1
  46. package/dist/ui/safe-area.d.ts +9 -0
  47. package/dist/ui/safe-area.d.ts.map +1 -0
  48. package/dist/ui/safe-area.js +28 -0
  49. package/dist/ui/safe-area.js.map +1 -0
  50. package/package.json +11 -3
  51. package/src/api/conversation-api.ts +33 -8
  52. package/src/hooks/use-live-chat.ts +3 -0
  53. package/src/hooks/use-send-message.ts +169 -159
  54. package/src/index.ts +3 -0
  55. package/src/navigation/LiveChatPanel.tsx +26 -3
  56. package/src/provider/LiveChatContext.ts +2 -0
  57. package/src/provider/LiveChatProvider.tsx +396 -380
  58. package/src/provider/types.ts +63 -57
  59. package/src/ui/components/ConversationHeader.tsx +7 -6
  60. package/src/ui/components/ConversationListScreen.tsx +18 -5
  61. package/src/ui/components/ConversationScreen.tsx +369 -362
  62. package/src/ui/components/HomeScreen.tsx +21 -6
  63. package/src/ui/components/MessageComposer.tsx +116 -36
  64. 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,24 @@ 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;
13
20
  };
14
21
 
22
+ let PanelSafeAreaProvider: ComponentType<{ children: ReactNode }> | null = null;
23
+ try {
24
+ PanelSafeAreaProvider = require("react-native-safe-area-context").SafeAreaProvider;
25
+ } catch {
26
+ PanelSafeAreaProvider = null;
27
+ }
28
+
15
29
  // Mirrors web PanelShell — neutral chrome for chat screens, brand bg for home
16
30
  function usePanelBackground(view: string): string {
17
31
  const { widgetConfig } = useLiveChatContext();
@@ -104,13 +118,22 @@ function PanelContent({ onClose }: LiveChatPanelProps) {
104
118
  }
105
119
 
106
120
  export function LiveChatPanel({ onClose }: LiveChatPanelProps) {
107
- return (
121
+ const panel = (
108
122
  <PanelRouterProvider>
109
123
  <View style={styles.flex}>
110
124
  <PanelContent onClose={onClose} />
111
125
  </View>
112
126
  </PanelRouterProvider>
113
127
  );
128
+
129
+ const withKeyboard = KeyboardProvider ? (
130
+ <KeyboardProvider>{panel}</KeyboardProvider>
131
+ ) : panel;
132
+
133
+ if (PanelSafeAreaProvider) {
134
+ return <PanelSafeAreaProvider>{withKeyboard}</PanelSafeAreaProvider>;
135
+ }
136
+ return withKeyboard;
114
137
  }
115
138
 
116
139
  const styles = StyleSheet.create({
@@ -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,