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.
- package/CHANGELOG.md +30 -0
- package/README.md +91 -0
- package/dist/api/conversation-api.d.ts +16 -0
- package/dist/api/conversation-api.d.ts.map +1 -0
- package/dist/api/conversation-api.js +44 -0
- package/dist/api/conversation-api.js.map +1 -0
- package/dist/api/file-api.d.ts +5 -0
- package/dist/api/file-api.d.ts.map +1 -0
- package/dist/api/file-api.js +15 -0
- package/dist/api/file-api.js.map +1 -0
- package/dist/api/widget-api.d.ts +4 -0
- package/dist/api/widget-api.d.ts.map +1 -0
- package/dist/api/widget-api.js +15 -0
- package/dist/api/widget-api.js.map +1 -0
- package/dist/axios/axios.d.ts +32 -0
- package/dist/axios/axios.d.ts.map +1 -0
- package/dist/axios/axios.js +120 -0
- package/dist/axios/axios.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/http-client.d.ts +33 -0
- package/dist/core/http-client.d.ts.map +1 -0
- package/dist/core/http-client.js +104 -0
- package/dist/core/http-client.js.map +1 -0
- package/dist/core/identity.d.ts +7 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +62 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/visitor-params.d.ts +15 -0
- package/dist/core/visitor-params.d.ts.map +1 -0
- package/dist/core/visitor-params.js +45 -0
- package/dist/core/visitor-params.js.map +1 -0
- package/dist/hooks/use-conversations.d.ts +12 -0
- package/dist/hooks/use-conversations.d.ts.map +1 -0
- package/dist/hooks/use-conversations.js +177 -0
- package/dist/hooks/use-conversations.js.map +1 -0
- package/dist/hooks/use-live-chat.d.ts +30 -0
- package/dist/hooks/use-live-chat.d.ts.map +1 -0
- package/dist/hooks/use-live-chat.js +52 -0
- package/dist/hooks/use-live-chat.js.map +1 -0
- package/dist/hooks/use-messages.d.ts +11 -0
- package/dist/hooks/use-messages.d.ts.map +1 -0
- package/dist/hooks/use-messages.js +185 -0
- package/dist/hooks/use-messages.js.map +1 -0
- package/dist/hooks/use-send-message.d.ts +22 -0
- package/dist/hooks/use-send-message.d.ts.map +1 -0
- package/dist/hooks/use-send-message.js +125 -0
- package/dist/hooks/use-send-message.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/LiveChatPanel.d.ts +5 -0
- package/dist/navigation/LiveChatPanel.d.ts.map +1 -0
- package/dist/navigation/LiveChatPanel.js +81 -0
- package/dist/navigation/LiveChatPanel.js.map +1 -0
- package/dist/navigation/panel-router-context.d.ts +22 -0
- package/dist/navigation/panel-router-context.d.ts.map +1 -0
- package/dist/navigation/panel-router-context.js +42 -0
- package/dist/navigation/panel-router-context.js.map +1 -0
- package/dist/navigation/router-types.d.ts +2 -0
- package/dist/navigation/router-types.d.ts.map +1 -0
- package/dist/navigation/router-types.js +3 -0
- package/dist/navigation/router-types.js.map +1 -0
- package/dist/provider/LiveChatContext.d.ts +4 -0
- package/dist/provider/LiveChatContext.d.ts.map +1 -0
- package/dist/provider/LiveChatContext.js +35 -0
- package/dist/provider/LiveChatContext.js.map +1 -0
- package/dist/provider/LiveChatProvider.d.ts +3 -0
- package/dist/provider/LiveChatProvider.d.ts.map +1 -0
- package/dist/provider/LiveChatProvider.js +308 -0
- package/dist/provider/LiveChatProvider.js.map +1 -0
- package/dist/provider/types.d.ts +42 -0
- package/dist/provider/types.d.ts.map +1 -0
- package/dist/provider/types.js +3 -0
- package/dist/provider/types.js.map +1 -0
- package/dist/realtime/ws-client.d.ts +51 -0
- package/dist/realtime/ws-client.d.ts.map +1 -0
- package/dist/realtime/ws-client.js +322 -0
- package/dist/realtime/ws-client.js.map +1 -0
- package/dist/ui/components/AssigneeAvatar.d.ts +12 -0
- package/dist/ui/components/AssigneeAvatar.d.ts.map +1 -0
- package/dist/ui/components/AssigneeAvatar.js +58 -0
- package/dist/ui/components/AssigneeAvatar.js.map +1 -0
- package/dist/ui/components/Avatar.d.ts +10 -0
- package/dist/ui/components/Avatar.d.ts.map +1 -0
- package/dist/ui/components/Avatar.js +76 -0
- package/dist/ui/components/Avatar.js.map +1 -0
- package/dist/ui/components/ConversationHeader.d.ts +10 -0
- package/dist/ui/components/ConversationHeader.d.ts.map +1 -0
- package/dist/ui/components/ConversationHeader.js +90 -0
- package/dist/ui/components/ConversationHeader.js.map +1 -0
- package/dist/ui/components/ConversationListScreen.d.ts +9 -0
- package/dist/ui/components/ConversationListScreen.d.ts.map +1 -0
- package/dist/ui/components/ConversationListScreen.js +350 -0
- package/dist/ui/components/ConversationListScreen.js.map +1 -0
- package/dist/ui/components/ConversationScreen.d.ts +8 -0
- package/dist/ui/components/ConversationScreen.d.ts.map +1 -0
- package/dist/ui/components/ConversationScreen.js +235 -0
- package/dist/ui/components/ConversationScreen.js.map +1 -0
- package/dist/ui/components/HomeScreen.d.ts +6 -0
- package/dist/ui/components/HomeScreen.d.ts.map +1 -0
- package/dist/ui/components/HomeScreen.js +133 -0
- package/dist/ui/components/HomeScreen.js.map +1 -0
- package/dist/ui/components/LivechatMessageRenderer.d.ts +17 -0
- package/dist/ui/components/LivechatMessageRenderer.d.ts.map +1 -0
- package/dist/ui/components/LivechatMessageRenderer.js +122 -0
- package/dist/ui/components/LivechatMessageRenderer.js.map +1 -0
- package/dist/ui/components/LogMessage.d.ts +5 -0
- package/dist/ui/components/LogMessage.d.ts.map +1 -0
- package/dist/ui/components/LogMessage.js +83 -0
- package/dist/ui/components/LogMessage.js.map +1 -0
- package/dist/ui/components/MessageBubble.d.ts +15 -0
- package/dist/ui/components/MessageBubble.d.ts.map +1 -0
- package/dist/ui/components/MessageBubble.js +84 -0
- package/dist/ui/components/MessageBubble.js.map +1 -0
- package/dist/ui/components/MessageComposer.d.ts +31 -0
- package/dist/ui/components/MessageComposer.d.ts.map +1 -0
- package/dist/ui/components/MessageComposer.js +295 -0
- package/dist/ui/components/MessageComposer.js.map +1 -0
- package/dist/ui/components/WsStatusStrip.d.ts +2 -0
- package/dist/ui/components/WsStatusStrip.d.ts.map +1 -0
- package/dist/ui/components/WsStatusStrip.js +103 -0
- package/dist/ui/components/WsStatusStrip.js.map +1 -0
- package/dist/ui/icons.d.ts +22 -0
- package/dist/ui/icons.d.ts.map +1 -0
- package/dist/ui/icons.js +71 -0
- package/dist/ui/icons.js.map +1 -0
- package/dist/ui/theme.d.ts +72 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +170 -0
- package/dist/ui/theme.js.map +1 -0
- package/docs/backend-contract.md +392 -0
- package/docs/migration-notes.md +32 -0
- package/package.json +60 -0
- package/src/api/conversation-api.ts +71 -0
- package/src/api/file-api.ts +14 -0
- package/src/api/widget-api.ts +12 -0
- package/src/axios/axios.ts +159 -0
- package/src/core/config.ts +54 -0
- package/src/core/http-client.ts +136 -0
- package/src/core/identity.ts +68 -0
- package/src/core/visitor-params.ts +48 -0
- package/src/hooks/use-conversations.ts +181 -0
- package/src/hooks/use-live-chat.ts +84 -0
- package/src/hooks/use-messages.ts +188 -0
- package/src/hooks/use-send-message.ts +159 -0
- package/src/index.ts +114 -0
- package/src/navigation/LiveChatPanel.tsx +118 -0
- package/src/navigation/panel-router-context.tsx +89 -0
- package/src/navigation/router-types.ts +1 -0
- package/src/provider/LiveChatContext.ts +33 -0
- package/src/provider/LiveChatProvider.tsx +380 -0
- package/src/provider/types.ts +57 -0
- package/src/realtime/ws-client.ts +369 -0
- package/src/types/react-native-svg.d.ts +10 -0
- package/src/ui/components/AssigneeAvatar.tsx +102 -0
- package/src/ui/components/Avatar.tsx +110 -0
- package/src/ui/components/ConversationHeader.tsx +202 -0
- package/src/ui/components/ConversationListScreen.tsx +454 -0
- package/src/ui/components/ConversationScreen.tsx +362 -0
- package/src/ui/components/HomeScreen.tsx +278 -0
- package/src/ui/components/LivechatMessageRenderer.tsx +268 -0
- package/src/ui/components/LogMessage.tsx +88 -0
- package/src/ui/components/MessageBubble.tsx +148 -0
- package/src/ui/components/MessageComposer.tsx +461 -0
- package/src/ui/components/WsStatusStrip.tsx +123 -0
- package/src/ui/icons.tsx +111 -0
- 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
|
+
});
|