@tellescope/chat 1.2.5 → 1.3.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 (47) hide show
  1. package/lib/cjs/chat.d.ts +3 -13
  2. package/lib/cjs/chat.d.ts.map +1 -1
  3. package/lib/cjs/chat.js +32 -63
  4. package/lib/cjs/chat.js.map +1 -1
  5. package/lib/cjs/components.d.ts +15 -0
  6. package/lib/cjs/components.d.ts.map +1 -0
  7. package/lib/cjs/components.js +76 -0
  8. package/lib/cjs/components.js.map +1 -0
  9. package/lib/cjs/components.native.d.ts +9 -0
  10. package/lib/cjs/components.native.d.ts.map +1 -0
  11. package/lib/cjs/components.native.js +169 -0
  12. package/lib/cjs/components.native.js.map +1 -0
  13. package/lib/cjs/index.d.ts +1 -0
  14. package/lib/cjs/index.d.ts.map +1 -1
  15. package/lib/cjs/index.js +1 -0
  16. package/lib/cjs/index.js.map +1 -1
  17. package/lib/cjs/index.native.d.ts +1 -0
  18. package/lib/cjs/index.native.d.ts.map +1 -1
  19. package/lib/cjs/index.native.js +1 -0
  20. package/lib/cjs/index.native.js.map +1 -1
  21. package/lib/esm/chat.d.ts +3 -13
  22. package/lib/esm/chat.d.ts.map +1 -1
  23. package/lib/esm/chat.js +31 -38
  24. package/lib/esm/chat.js.map +1 -1
  25. package/lib/esm/components.d.ts +15 -0
  26. package/lib/esm/components.d.ts.map +1 -0
  27. package/lib/esm/components.js +49 -0
  28. package/lib/esm/components.js.map +1 -0
  29. package/lib/esm/components.native.d.ts +9 -0
  30. package/lib/esm/components.native.d.ts.map +1 -0
  31. package/lib/esm/components.native.js +165 -0
  32. package/lib/esm/components.native.js.map +1 -0
  33. package/lib/esm/index.d.ts +1 -0
  34. package/lib/esm/index.d.ts.map +1 -1
  35. package/lib/esm/index.js +1 -0
  36. package/lib/esm/index.js.map +1 -1
  37. package/lib/esm/index.native.d.ts +1 -0
  38. package/lib/esm/index.native.d.ts.map +1 -1
  39. package/lib/esm/index.native.js +1 -0
  40. package/lib/esm/index.native.js.map +1 -1
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +9 -8
  43. package/src/chat.tsx +49 -93
  44. package/src/components.native.tsx +174 -0
  45. package/src/components.tsx +88 -0
  46. package/src/index.native.ts +2 -1
  47. package/src/index.ts +2 -1
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@tellescope/chat",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
7
+ "react-native": "./lib/esm/index.js",
7
8
  "types": "./lib/esm/index.d.ts",
8
9
  "scripts": {
9
10
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -33,13 +34,13 @@
33
34
  "@fontsource/roboto": "^4.5.1",
34
35
  "@mui/icons-material": "^5.0.1",
35
36
  "@mui/material": "^5.0.2",
36
- "@tellescope/constants": "^1.2.5",
37
- "@tellescope/react-components": "^1.2.5",
38
- "@tellescope/sdk": "^1.2.5",
39
- "@tellescope/types-client": "^1.2.5",
40
- "@tellescope/types-models": "^1.2.5",
37
+ "@tellescope/constants": "^1.3.0",
38
+ "@tellescope/react-components": "^1.3.0",
39
+ "@tellescope/sdk": "^1.3.0",
40
+ "@tellescope/types-client": "^1.3.0",
41
+ "@tellescope/types-models": "^1.3.0",
41
42
  "@tellescope/types-utilities": "^1.2.2",
42
- "@tellescope/utilities": "^1.2.5",
43
+ "@tellescope/utilities": "^1.3.0",
43
44
  "@typescript-eslint/eslint-plugin": "^4.33.0",
44
45
  "@typescript-eslint/parser": "^4.33.0",
45
46
  "eslint": "^7.32.0",
@@ -50,7 +51,7 @@
50
51
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
51
52
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
52
53
  },
53
- "gitHead": "f1e12f7af228ec1768e3183c16b7164640a4978c",
54
+ "gitHead": "8635df935a3bb2562c8308341626488cb84dd8c4",
54
55
  "publishConfig": {
55
56
  "access": "public"
56
57
  }
package/src/chat.tsx CHANGED
@@ -1,16 +1,13 @@
1
- import React, { useState, CSSProperties, useEffect } from "react"
1
+ import React, { useState, CSSProperties, useEffect, useRef } from "react"
2
2
 
3
3
  import {
4
- AsyncIconButton,
5
4
  DisplayPicture,
6
5
  IN_REACT_WEB,
7
6
  List,
8
7
  Flex,
9
8
  Badge,
10
- SendIcon,
11
9
  Styled,
12
10
  Typography,
13
- TextField,
14
11
  LoadingLinear,
15
12
  value_is_loaded,
16
13
  useSession,
@@ -22,6 +19,7 @@ import {
22
19
  ChatRoomDisplayInfo,
23
20
  SecureImage,
24
21
  ImageDimensions,
22
+ SecureVideo,
25
23
  } from "@tellescope/react-components"
26
24
 
27
25
  import {
@@ -52,6 +50,7 @@ import {
52
50
  Session,
53
51
  EnduserSession,
54
52
  } from "@tellescope/sdk"
53
+ import { SendMessage } from "./components"
55
54
 
56
55
  export {
57
56
  user_display_name, // for convenience
@@ -184,12 +183,23 @@ export const Message = ({
184
183
  export const MessageAttachments = ({ message, chatUserId, imageDimensions } : { message: ChatMessage, chatUserId: string, imageDimensions?: ImageDimensions }) => {
185
184
  if (!message.attachments) return null
186
185
  if (message.attachments.length === 0) return null
187
-
186
+
188
187
  return (
189
188
  <Flex column alignSelf={message.senderId === chatUserId ? "flex-end" : "flex-start"}>
190
189
  {message.attachments.filter(a => a.type === 'image').map(a=> (
191
- <Flex key={a.secureName} style={{ margin: 10 }}>
192
- <SecureImage key={a.secureName} secureName={a.secureName} alt="image attachment" {...imageDimensions} />
190
+ <Flex key={a.secureName} style={{
191
+ marginRight: message.senderId === chatUserId ? 0 : 10,
192
+ marginLeft: message.senderId === chatUserId ? 10 : 0,
193
+ justifyContent: message.senderId === chatUserId ? "flex-end" : "flex-start",
194
+ }}>
195
+ <SecureImage secureName={a.secureName} alt="image attachment" {...imageDimensions} />
196
+ </Flex>
197
+ ))}
198
+ {message.attachments.filter(a => a.type === 'video').map(a=> (
199
+ <Flex key={a.secureName} style={{
200
+ justifyContent: message.senderId === chatUserId ? "flex-end" : "flex-start",
201
+ }}>
202
+ <SecureVideo secureName={a.secureName} {...imageDimensions} />
193
203
  </Flex>
194
204
  ))}
195
205
  </Flex>
@@ -202,6 +212,7 @@ interface Messages_T extends MessageStyles {
202
212
  chatUserId: string,
203
213
  headerProps?: MessagesHeaderProps,
204
214
  imageDimensions?: ImageDimensions,
215
+ markRead?: boolean,
205
216
  }
206
217
  export const MessagesWithHeader = ({
207
218
  resolveSenderName,
@@ -212,7 +223,7 @@ export const MessagesWithHeader = ({
212
223
  style,
213
224
  imageDimensions,
214
225
  ...messageStyles
215
- }: Messages_T & Styled & { Header?: React.JSXElementConstructor<MessagesHeaderProps> }) => (
226
+ }: Omit<Messages_T, 'markRead'> & Styled & { Header?: React.JSXElementConstructor<MessagesHeaderProps> }) => (
216
227
  <LoadingLinear data={messages} render={messages => (
217
228
  <Flex column flex={1} style={{ ...style, overflowY: 'scroll' }}>
218
229
  {Header && <Header {...headerProps}/>}
@@ -232,14 +243,37 @@ export const Messages = ({
232
243
  headerProps,
233
244
  style,
234
245
  imageDimensions,
246
+ markRead,
235
247
  ...messageStyles
236
- }: Messages_T & Styled) => (
237
- <LoadingLinear data={messages} render={messages => (
238
- <List reverse items={messages} style={style} render={(message) => (
239
- <Message message={message} imageDimensions={imageDimensions} {...messageStyles} />
240
- )}/>
241
- )}/>
242
- )
248
+ }: Messages_T & Styled) => {
249
+ const session = useResolvedSession()
250
+ const [, { updateLocalElement: updateRoom }] = useChatRooms()
251
+ const markReadRef = useRef(false)
252
+
253
+ useEffect(() => {
254
+ if (!markRead) return
255
+ if (!value_is_loaded(messages)) return
256
+ if (messages.value.length === 0) return
257
+
258
+ if (markReadRef.current) return
259
+ markReadRef.current = true
260
+
261
+ session.api.chat_rooms.mark_read({ id: messages.value[0].roomId })
262
+ .then(({ updated }) => {
263
+ updateRoom(updated.id, updated)
264
+ })
265
+ .catch(console.error)
266
+ }, [session, markRead, messages, markReadRef, updateRoom])
267
+
268
+
269
+ return (
270
+ <LoadingLinear data={messages} render={messages => (
271
+ <List reverse items={messages} style={style} render={(message) => (
272
+ <Message message={message} imageDimensions={imageDimensions} {...messageStyles} />
273
+ )}/>
274
+ )}/>
275
+ )
276
+ }
243
277
 
244
278
  const defaultSidebarStyle: CSSProperties = {
245
279
  borderRadius: 5,
@@ -351,84 +385,6 @@ export const EndusersConversations = ({ enduserId, ...p } : SidebarInfo & { endu
351
385
  // deprecated while Conversations relies on useResolvedSession
352
386
  export const UsersConversations = ({ userId, ...p } : SidebarInfo & { userId: string }) => <Conversations {...p} rooms={useChatRooms()[0]}/>
353
387
 
354
- interface SendMessage_T {
355
- roomId: string,
356
- onNewMessage?: (m: ChatMessage) => void;
357
- placeholderText?: string;
358
- Icon?: React.ElementType<any>;
359
- style?: CSSProperties;
360
-
361
- // web only
362
- sendOnEnterPress?: boolean,
363
- multiline?: boolean,
364
- maxRows?: number,
365
- }
366
- export const SendMessage = ({
367
- roomId,
368
- Icon=SendIcon,
369
- onNewMessage,
370
- placeholderText="Enter a message",
371
- style={},
372
- sendOnEnterPress,
373
- multiline,
374
- maxRows,
375
- }: SendMessage_T) => {
376
- const [message, setMessage] = useState('')
377
- const [sending, setSending] = useState(false)
378
-
379
- const [disabled, setDisabled] = useState(false)
380
- const [chatFocused, setChatFocused] = React.useState(false)
381
-
382
- const [, { createElement: createMessage }] = useChats(roomId)
383
-
384
- useEffect(() => {
385
- if (!chatFocused) return
386
- if (!sendOnEnterPress) return
387
- if (typeof window === 'undefined') return
388
-
389
- const handleSend = (e: any) => {
390
- if (e.key !== 'Enter') return
391
- setDisabled(true)
392
-
393
- createMessage({ message, roomId })
394
- .then(m => {
395
- setMessage('')
396
- onNewMessage?.(m)
397
- })
398
- .catch(console.error)
399
- .finally(() => setDisabled(false))
400
- }
401
-
402
- window.addEventListener('keypress', handleSend)
403
- return () => { window.removeEventListener('keypress', handleSend) }
404
- }, [sendOnEnterPress, chatFocused, message, roomId])
405
-
406
- return (
407
- <Flex row flex={1} alignContent="center" style={style}>
408
- <Flex column flex={1}>
409
- <TextField variant="outlined" value={message} onChange={setMessage} disabled={sending}
410
- aria-label="Enter a message"
411
- multiline={multiline} maxRows={maxRows}
412
- placeholder={placeholderText}
413
- onFocus={() => setChatFocused(true)}
414
- onBlur={() => setChatFocused(false)}
415
- />
416
- </Flex>
417
- <Flex column alignSelf="center">
418
- <AsyncIconButton label="send" Icon={Icon}
419
- disabled={message === '' || disabled}
420
- action={() => createMessage({ message, roomId })}
421
- onSuccess={m => {
422
- setMessage('')
423
- onNewMessage?.(m)
424
- }}
425
- onChange={setSending}
426
- />
427
- </Flex>
428
- </Flex>
429
- )
430
- }
431
-
432
388
  const defaultSplitChatStyle: CSSProperties = {}
433
389
  interface SplitChat_T {
434
390
  session: EnduserSession | Session,
@@ -0,0 +1,174 @@
1
+ import { launchImageLibrary, launchCamera } from 'react-native-image-picker';
2
+ import { Modal, View, TextInput } from "react-native"
3
+ import { Avatar } from 'react-native-paper'
4
+ import {
5
+ useFileUpload,
6
+ Flex,
7
+ Typography,
8
+ useEnduserSession,
9
+ AsyncIconButton,
10
+ SendIcon,
11
+ LabeledIconButton,
12
+ Button,
13
+ Paper,
14
+ useChats,
15
+ } from "@tellescope/react-components"
16
+ import { useState } from 'react';
17
+
18
+ const CHAT_ICON_SIZE = 35
19
+ const SendImageOrVideo = ({
20
+ onUpload,
21
+ disabled
22
+ } : {
23
+ disabled?: boolean,
24
+ onUpload: (file: { secureName: string }, assetType: 'image' | 'video') => void,
25
+ }) => {
26
+ const session = useEnduserSession()
27
+ const [menuOpen, setMenuOpen] = useState(false)
28
+ const [errorMessage, setErrorMessage] = useState('')
29
+ const { handleUpload, uploading } = useFileUpload({ enduserId: session.userInfo.id })
30
+
31
+ const handleImageSelect = async (type: 'library' | 'camera') => {
32
+ if (uploading) return
33
+ setErrorMessage('')
34
+
35
+ const result = await (type === 'library' ? launchImageLibrary : launchCamera)({
36
+ mediaType: 'mixed', // library' ? 'mixed' : 'photo', // mixed only works for library
37
+ maxWidth: 500,
38
+ maxHeight: 500,
39
+ })
40
+ if (result.didCancel) return
41
+ if (result.errorCode || result.errorMessage) {
42
+ setErrorMessage(
43
+ result.errorCode === 'camera_unavailable' ? "Your camera is unavailable"
44
+ : result.errorCode === 'permission' ? "We do have permission to access your camera"
45
+ : result.errorMessage || 'An error occurred'
46
+ )
47
+ return
48
+ }
49
+ const image = result.assets?.[0]
50
+ if (!image) return
51
+
52
+ const blob = await (await fetch(image.uri as string)).blob()
53
+ const actualSize = blob.size // image.size is incorrect by an unpredictable margin
54
+
55
+ try {
56
+ const file = await handleUpload({
57
+ name: image.fileName as string,
58
+ size: actualSize, // image.fileSize as number,
59
+ type: image.type as string,
60
+ }, {
61
+ uri: image.uri as string,
62
+ name: image.fileName as string,
63
+ type: image.type as string,
64
+ })
65
+ onUpload(
66
+ file,
67
+ result.assets?.[0]?.duration ? 'video' : 'image',
68
+ )
69
+ setMenuOpen(false)
70
+ } catch(err: any) {
71
+ setErrorMessage(err?.message ?? err.toString())
72
+ }
73
+ }
74
+
75
+ const menuDisabled = uploading || disabled
76
+ return (
77
+ <>
78
+ <Modal animationType='fade' transparent={true}
79
+ visible={menuOpen} onRequestClose={() => setMenuOpen(false)}
80
+ >
81
+ <Flex flex={1} alignItems="center" justifyContent="center">
82
+ <Paper flex style={{ padding: 25, margin: 10, }} >
83
+ <Button disabled={menuDisabled} variant="contained" onPress={() => handleImageSelect('library')}
84
+ style={{ marginBottom: 20 }}
85
+ >
86
+ <View style={{ flexDirection: 'row', alignItems: 'center', minHeight: 40 }}>
87
+ <Typography style={{ fontSize: 18, marginRight: 10 }}>Upload</Typography>
88
+ <Avatar.Icon icon="image" size={CHAT_ICON_SIZE} />
89
+ </View>
90
+ </Button>
91
+ <Button disabled={menuDisabled} variant="contained" onPress={() => handleImageSelect('camera')}>
92
+ <View style={{ flexDirection: 'row', alignItems: 'center', minHeight: 40 }}>
93
+ <Typography style={{ fontSize: 18, marginRight: 10 }}>Take Photo</Typography>
94
+ <Avatar.Icon icon="camera" size={CHAT_ICON_SIZE} />
95
+ </View>
96
+ </Button>
97
+ <Button variant='outlined' onPress={() => setMenuOpen(false)} style={{ marginTop: 25 }}>
98
+ Cancel
99
+ </Button>
100
+
101
+ <Typography style={{ marginTop: 5, color: '#ff4444'}}>
102
+ {errorMessage}
103
+ </Typography>
104
+ </Paper>
105
+ </Flex>
106
+ </Modal>
107
+
108
+ <LabeledIconButton label="attach image" disabled={menuDisabled}
109
+ onClick={() => setMenuOpen(true)}
110
+ Icon={() => <Avatar.Icon icon={'image'} size={CHAT_ICON_SIZE} />}
111
+ />
112
+ </>
113
+ )
114
+ }
115
+
116
+ interface SendMessage_T {
117
+ roomId: string,
118
+ placeholderText?: string;
119
+ style?: React.CSSProperties;
120
+ }
121
+ export const SendMessage = ({
122
+ roomId,
123
+ placeholderText="Enter a message",
124
+ style={},
125
+ }: SendMessage_T) => {
126
+ const [message, setMessage] = useState('')
127
+ const [sending, setSending] = useState(false)
128
+
129
+ const [, { createElement: createMessage }] = useChats(roomId)
130
+
131
+ return (
132
+ <Flex row flex={1} alignContent="center" style={{ marginTop: 8, ...style }}>
133
+ <Flex column flex={1} style={{ marginLeft: 5, marginRight: 5 }}>
134
+ <TextInput value={message} onChangeText={setMessage}
135
+ style={{
136
+ width: '100%',
137
+ borderColor: '#0f0f0f',
138
+ borderWidth: 1,
139
+ padding: 10,
140
+ borderRadius: 5,
141
+ }}
142
+ aria-label="Enter a message..."
143
+ placeholder={placeholderText}
144
+ />
145
+ </Flex>
146
+ <Flex alignSelf="center">
147
+ <Flex style={{ marginRight: 5 }}>
148
+ <AsyncIconButton label="send" Icon={() => <SendIcon size={CHAT_ICON_SIZE} />}
149
+ disabled={message === ''}
150
+ action={() => createMessage({ message, roomId })}
151
+ onSuccess={() => setMessage('')}
152
+ onChange={setSending}
153
+ />
154
+ </Flex>
155
+
156
+ <Flex style={{ marginRight: 5 }}>
157
+ <SendImageOrVideo disabled={sending} onUpload={async ({ secureName }, type) => {
158
+ setSending(true)
159
+ try {
160
+ await createMessage({
161
+ roomId, message: '',
162
+ attachments: [{ type, secureName }]
163
+ })
164
+ } catch(err: any) {
165
+ console.error(err)
166
+ } finally {
167
+ setSending(false)
168
+ }
169
+ }} />
170
+ </Flex>
171
+ </Flex>
172
+ </Flex>
173
+ )
174
+ }
@@ -0,0 +1,88 @@
1
+ import React, { CSSProperties, useEffect, useState } from "react"
2
+ import { ChatMessage } from "@tellescope/types-client";
3
+
4
+ import {
5
+ AsyncIconButton,
6
+ Flex,
7
+ SendIcon,
8
+ TextField,
9
+ useChats,
10
+ } from "@tellescope/react-components"
11
+
12
+ interface SendMessage_T {
13
+ roomId: string,
14
+ onNewMessage?: (m: ChatMessage) => void;
15
+ placeholderText?: string;
16
+ Icon?: React.ElementType<any>;
17
+ style?: CSSProperties;
18
+
19
+ // web only
20
+ sendOnEnterPress?: boolean,
21
+ multiline?: boolean,
22
+ maxRows?: number,
23
+ }
24
+ export const SendMessage = ({
25
+ roomId,
26
+ Icon=SendIcon,
27
+ onNewMessage,
28
+ placeholderText="Enter a message",
29
+ style={},
30
+ sendOnEnterPress,
31
+ multiline,
32
+ maxRows,
33
+ }: SendMessage_T) => {
34
+ const [message, setMessage] = useState('')
35
+ const [sending, setSending] = useState(false)
36
+
37
+ const [disabled, setDisabled] = useState(false)
38
+ const [chatFocused, setChatFocused] = React.useState(false)
39
+
40
+ const [, { createElement: createMessage }] = useChats(roomId)
41
+
42
+ useEffect(() => {
43
+ if (!chatFocused) return
44
+ if (!sendOnEnterPress) return
45
+ if (typeof window === 'undefined') return
46
+
47
+ const handleSend = (e: any) => {
48
+ if (e.key !== 'Enter') return
49
+ setDisabled(true)
50
+
51
+ createMessage({ message, roomId })
52
+ .then(m => {
53
+ setMessage('')
54
+ onNewMessage?.(m)
55
+ })
56
+ .catch(console.error)
57
+ .finally(() => setDisabled(false))
58
+ }
59
+
60
+ window.addEventListener('keypress', handleSend)
61
+ return () => { window.removeEventListener('keypress', handleSend) }
62
+ }, [sendOnEnterPress, chatFocused, message, roomId])
63
+
64
+ return (
65
+ <Flex row flex={1} alignContent="center" style={style}>
66
+ <Flex column flex={1}>
67
+ <TextField variant="outlined" value={message} onChange={setMessage} disabled={sending}
68
+ aria-label="Enter a message"
69
+ multiline={multiline} maxRows={maxRows}
70
+ placeholder={placeholderText}
71
+ onFocus={() => setChatFocused(true)}
72
+ onBlur={() => setChatFocused(false)}
73
+ />
74
+ </Flex>
75
+ <Flex column alignSelf="center">
76
+ <AsyncIconButton label="send" Icon={Icon}
77
+ disabled={message === '' || disabled}
78
+ action={() => createMessage({ message, roomId })}
79
+ onSuccess={m => {
80
+ setMessage('')
81
+ onNewMessage?.(m)
82
+ }}
83
+ onChange={setSending}
84
+ />
85
+ </Flex>
86
+ </Flex>
87
+ )
88
+ }
@@ -1 +1,2 @@
1
- export * from "./chat"
1
+ export * from "./chat"
2
+ export * from "./components"
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
- export * from "./chat";
1
+ export * from "./chat";
2
+ export * from "./components"