@tellescope/chat 1.2.3 → 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.
- package/lib/cjs/chat.d.ts +3 -13
- package/lib/cjs/chat.d.ts.map +1 -1
- package/lib/cjs/chat.js +32 -63
- package/lib/cjs/chat.js.map +1 -1
- package/lib/cjs/components.d.ts +15 -0
- package/lib/cjs/components.d.ts.map +1 -0
- package/lib/cjs/components.js +76 -0
- package/lib/cjs/components.js.map +1 -0
- package/lib/cjs/components.native.d.ts +9 -0
- package/lib/cjs/components.native.d.ts.map +1 -0
- package/lib/cjs/components.native.js +169 -0
- package/lib/cjs/components.native.js.map +1 -0
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/index.native.d.ts +1 -0
- package/lib/cjs/index.native.d.ts.map +1 -1
- package/lib/cjs/index.native.js +1 -0
- package/lib/cjs/index.native.js.map +1 -1
- package/lib/esm/chat.d.ts +3 -13
- package/lib/esm/chat.d.ts.map +1 -1
- package/lib/esm/chat.js +31 -38
- package/lib/esm/chat.js.map +1 -1
- package/lib/esm/components.d.ts +15 -0
- package/lib/esm/components.d.ts.map +1 -0
- package/lib/esm/components.js +49 -0
- package/lib/esm/components.js.map +1 -0
- package/lib/esm/components.native.d.ts +9 -0
- package/lib/esm/components.native.d.ts.map +1 -0
- package/lib/esm/components.native.js +165 -0
- package/lib/esm/components.native.js.map +1 -0
- package/lib/esm/index.d.ts +1 -0
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/index.native.d.ts +1 -0
- package/lib/esm/index.native.d.ts.map +1 -1
- package/lib/esm/index.native.js +1 -0
- package/lib/esm/index.native.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -8
- package/src/chat.tsx +49 -93
- package/src/components.native.tsx +174 -0
- package/src/components.tsx +88 -0
- package/src/index.native.ts +2 -1
- package/src/index.ts +2 -1
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/chat",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
37
|
-
"@tellescope/react-components": "^1.
|
|
38
|
-
"@tellescope/sdk": "^1.
|
|
39
|
-
"@tellescope/types-client": "^1.
|
|
40
|
-
"@tellescope/types-models": "^1.
|
|
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.
|
|
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": "
|
|
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={{
|
|
192
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
package/src/index.native.ts
CHANGED
|
@@ -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"
|