@connectycube/react-ui-kit 0.0.19 → 0.0.20
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/configs/dependencies.json +6 -0
- package/configs/imports.json +2 -0
- package/dist/index.cjs +1 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -35
- package/dist/index.js.map +1 -1
- package/dist/types/components/attachment.d.ts +3 -3
- package/dist/types/components/attachment.d.ts.map +1 -1
- package/dist/types/components/avatar.d.ts +1 -0
- package/dist/types/components/avatar.d.ts.map +1 -1
- package/dist/types/components/badge.d.ts +1 -1
- package/dist/types/components/button.d.ts +2 -2
- package/dist/types/components/call-message.d.ts +17 -0
- package/dist/types/components/call-message.d.ts.map +1 -0
- package/dist/types/components/chat-message.d.ts +30 -0
- package/dist/types/components/chat-message.d.ts.map +1 -0
- package/dist/types/components/dialog-item.d.ts.map +1 -1
- package/dist/types/components/linkify-text.d.ts +6 -1
- package/dist/types/components/linkify-text.d.ts.map +1 -1
- package/dist/types/components/switch.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/gen/components/attachment.jsx +21 -19
- package/gen/components/avatar.jsx +14 -2
- package/gen/components/call-message.jsx +62 -0
- package/gen/components/chat-message.jsx +120 -0
- package/gen/components/dialog-item.jsx +4 -1
- package/gen/components/linkify-text.jsx +41 -2
- package/gen/components/switch.jsx +0 -2
- package/gen/index.js +4 -0
- package/package.json +11 -10
- package/src/components/attachment.tsx +25 -26
- package/src/components/avatar.tsx +3 -1
- package/src/components/call-message.tsx +75 -0
- package/src/components/chat-message.tsx +138 -0
- package/src/components/connectycube-ui/attachment.tsx +269 -0
- package/src/components/connectycube-ui/chat-message.tsx +138 -0
- package/src/components/connectycube-ui/link-preview.tsx +149 -0
- package/src/components/dialog-item.tsx +4 -1
- package/src/components/linkify-text.tsx +44 -3
- package/src/components/switch.tsx +0 -2
- package/src/index.ts +6 -0
package/gen/index.js
CHANGED
|
@@ -14,6 +14,10 @@ export { Badge } from './components/badge';
|
|
|
14
14
|
|
|
15
15
|
export { Button } from './components/button';
|
|
16
16
|
|
|
17
|
+
export { CallMessage } from './components/call-message';
|
|
18
|
+
|
|
19
|
+
export { ChatMessage } from './components/chat-message';
|
|
20
|
+
|
|
17
21
|
export { DialogItem } from './components/dialog-item';
|
|
18
22
|
|
|
19
23
|
export { DismissLayer } from './components/dismiss-layer';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connectycube/react-ui-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Simple React UI Kit generator with TSX/JSX",
|
|
5
5
|
"homepage": "https://github.com/ConnectyCube/react-ui-kit#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -71,11 +71,9 @@
|
|
|
71
71
|
"class-variance-authority": "^0.7.1",
|
|
72
72
|
"clsx": "^2.1.1",
|
|
73
73
|
"date-fns": "^4.1.0",
|
|
74
|
-
"execa": "^9.6.0",
|
|
75
|
-
"fs-extra": "^11.3.2",
|
|
76
74
|
"linkify-react": "^4.3.2",
|
|
77
|
-
"lucide-react": "^0.
|
|
78
|
-
"
|
|
75
|
+
"lucide-react": "^0.561.0",
|
|
76
|
+
"react-intersection-observer": "^10.0.0",
|
|
79
77
|
"tailwind-merge": "^3.4.0"
|
|
80
78
|
},
|
|
81
79
|
"peerDependencies": {
|
|
@@ -85,25 +83,28 @@
|
|
|
85
83
|
"devDependencies": {
|
|
86
84
|
"@babel/core": "^7.28.5",
|
|
87
85
|
"@babel/preset-typescript": "^7.28.5",
|
|
88
|
-
"@eslint/js": "^9.39.
|
|
86
|
+
"@eslint/js": "^9.39.2",
|
|
89
87
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
90
88
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
91
89
|
"@rollup/plugin-terser": "^0.4.4",
|
|
92
90
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
93
91
|
"@stylistic/eslint-plugin": "^5.6.1",
|
|
94
|
-
"@types/node": "^
|
|
92
|
+
"@types/node": "^25.0.1",
|
|
95
93
|
"@types/react": "^19.2.7",
|
|
96
|
-
"eslint": "^9.39.
|
|
94
|
+
"eslint": "^9.39.2",
|
|
97
95
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
98
96
|
"eslint-plugin-react-refresh": "^0.4.24",
|
|
97
|
+
"execa": "^9.6.1",
|
|
99
98
|
"fast-glob": "^3.3.3",
|
|
99
|
+
"fs-extra": "^11.3.2",
|
|
100
100
|
"globals": "^16.5.0",
|
|
101
|
-
"prettier": "^3.
|
|
101
|
+
"prettier": "^3.7.4",
|
|
102
|
+
"prompts": "^2.4.2",
|
|
102
103
|
"rollup": "^4.53.3",
|
|
103
104
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
104
105
|
"tslib": "^2.8.1",
|
|
105
106
|
"typescript": "^5.9.3",
|
|
106
|
-
"typescript-eslint": "^8.
|
|
107
|
+
"typescript-eslint": "^8.49.0"
|
|
107
108
|
},
|
|
108
109
|
"engines": {
|
|
109
110
|
"node": ">=18"
|
|
@@ -8,25 +8,22 @@ interface AttachmentProps {
|
|
|
8
8
|
uid?: string;
|
|
9
9
|
url?: string;
|
|
10
10
|
mimeType?: string;
|
|
11
|
-
|
|
11
|
+
pending?: boolean;
|
|
12
12
|
onReady?: (skipOnce?: boolean) => void;
|
|
13
13
|
linkProps?: AttachmentLinkProps;
|
|
14
14
|
containerProps?: React.ComponentProps<'div'>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
interface AttachmentLinkProps
|
|
18
|
-
extends React.ComponentProps<'a'>,
|
|
19
|
-
Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
|
|
18
|
+
extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
|
|
20
19
|
children?: React.ReactNode;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
interface AttachmentImageProps
|
|
24
|
-
extends React.ComponentProps<'img'>,
|
|
25
|
-
Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
|
|
23
|
+
extends React.ComponentProps<'img'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
|
|
26
24
|
|
|
27
25
|
interface AttachmentAudioProps
|
|
28
|
-
extends React.ComponentProps<'audio'>,
|
|
29
|
-
Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
|
|
26
|
+
extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
|
|
30
27
|
|
|
31
28
|
interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
|
|
32
29
|
maxSize?: number;
|
|
@@ -43,7 +40,7 @@ interface AttachmentFailedProps extends LucideProps, Omit<AttachmentProps, 'link
|
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
function AttachmentLinkBase(
|
|
46
|
-
{ url,
|
|
43
|
+
{ url, pending = false, children, ...props }: AttachmentLinkProps,
|
|
47
44
|
ref: React.ForwardedRef<HTMLAnchorElement>
|
|
48
45
|
) {
|
|
49
46
|
return (
|
|
@@ -54,12 +51,12 @@ function AttachmentLinkBase(
|
|
|
54
51
|
{...props}
|
|
55
52
|
href={url}
|
|
56
53
|
className={cn(
|
|
57
|
-
'group relative min-h-
|
|
54
|
+
'group relative min-h-8 min-w-8 w-full flex items-center justify-center rounded-md overflow-hidden bg-ring/10 hover:bg-ring/20 transition-color duration-300 ease-out cursor-pointer',
|
|
58
55
|
props?.className
|
|
59
56
|
)}
|
|
60
57
|
>
|
|
61
58
|
{children}
|
|
62
|
-
<Spinner loading={
|
|
59
|
+
<Spinner loading={pending} layout="overlay" />
|
|
63
60
|
</a>
|
|
64
61
|
);
|
|
65
62
|
}
|
|
@@ -69,7 +66,7 @@ const AttachmentLink = forwardRef<HTMLAnchorElement, AttachmentLinkProps>(Attach
|
|
|
69
66
|
AttachmentLink.displayName = 'AttachmentLink';
|
|
70
67
|
|
|
71
68
|
function AttachmentAudioBase(
|
|
72
|
-
{ uid, url,
|
|
69
|
+
{ uid, url, pending = false, containerProps, ...props }: AttachmentAudioProps,
|
|
73
70
|
ref: React.ForwardedRef<HTMLAudioElement>
|
|
74
71
|
) {
|
|
75
72
|
const audioId = `attachment_audio_${uid || getRandomString()}`;
|
|
@@ -77,10 +74,10 @@ function AttachmentAudioBase(
|
|
|
77
74
|
return (
|
|
78
75
|
<div
|
|
79
76
|
{...containerProps}
|
|
80
|
-
className={cn('relative min-h-
|
|
77
|
+
className={cn('relative min-h-8 min-w-8 w-full rounded-md overflow-hidden', containerProps?.className)}
|
|
81
78
|
>
|
|
82
79
|
<audio ref={ref} src={url} id={audioId} controls {...props} />
|
|
83
|
-
<Spinner loading={
|
|
80
|
+
<Spinner loading={pending} layout="overlay" />
|
|
84
81
|
</div>
|
|
85
82
|
);
|
|
86
83
|
}
|
|
@@ -88,7 +85,7 @@ function AttachmentAudioBase(
|
|
|
88
85
|
const AttachmentAudio = forwardRef<HTMLAudioElement, AttachmentAudioProps>(AttachmentAudioBase);
|
|
89
86
|
|
|
90
87
|
function AttachmentVideoBase(
|
|
91
|
-
{ uid, url, maxSize = 360,
|
|
88
|
+
{ uid, url, maxSize = 360, pending = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
|
|
92
89
|
ref: React.ForwardedRef<HTMLVideoElement>
|
|
93
90
|
) {
|
|
94
91
|
const videoId = `attachment_video_${uid || getRandomString()}`;
|
|
@@ -131,7 +128,7 @@ function AttachmentVideoBase(
|
|
|
131
128
|
onCanPlay={handleCanPlay}
|
|
132
129
|
className={cn('size-full', props?.className)}
|
|
133
130
|
/>
|
|
134
|
-
<Spinner loading={
|
|
131
|
+
<Spinner loading={pending} layout="overlay" />
|
|
135
132
|
</div>
|
|
136
133
|
);
|
|
137
134
|
}
|
|
@@ -141,7 +138,7 @@ const AttachmentVideo = forwardRef<HTMLVideoElement, AttachmentVideoProps>(Attac
|
|
|
141
138
|
AttachmentVideo.displayName = 'AttachmentVideo';
|
|
142
139
|
|
|
143
140
|
function AttachmentImageBase(
|
|
144
|
-
{ uid, url,
|
|
141
|
+
{ uid, url, pending = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
|
|
145
142
|
ref: React.ForwardedRef<HTMLImageElement>
|
|
146
143
|
) {
|
|
147
144
|
const imageId = `attachment_image_${uid || getRandomString()}`;
|
|
@@ -151,7 +148,7 @@ function AttachmentImageBase(
|
|
|
151
148
|
};
|
|
152
149
|
|
|
153
150
|
return (
|
|
154
|
-
<AttachmentLink href={url}
|
|
151
|
+
<AttachmentLink href={url} pending={pending} {...linkProps}>
|
|
155
152
|
<img
|
|
156
153
|
ref={ref}
|
|
157
154
|
src={url}
|
|
@@ -159,7 +156,7 @@ function AttachmentImageBase(
|
|
|
159
156
|
alt="attachment"
|
|
160
157
|
{...props}
|
|
161
158
|
className={cn(
|
|
162
|
-
'rounded-md object-cover min-h-
|
|
159
|
+
'rounded-md object-cover min-h-8 min-w-8 max-h-[360px] group-hover:scale-102 transition-transform duration-300 ease-out',
|
|
163
160
|
props?.className
|
|
164
161
|
)}
|
|
165
162
|
onLoad={handleLoad}
|
|
@@ -172,28 +169,28 @@ const AttachmentImage = forwardRef<HTMLImageElement, AttachmentImageProps>(Attac
|
|
|
172
169
|
|
|
173
170
|
AttachmentImage.displayName = 'AttachmentImage';
|
|
174
171
|
|
|
175
|
-
function AttachmentFile({ url, name,
|
|
172
|
+
function AttachmentFile({ url, name, pending = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
|
|
176
173
|
const fileId = `attachment_file_${props.id || getRandomString()}`;
|
|
177
174
|
|
|
178
175
|
return (
|
|
179
176
|
<AttachmentLink
|
|
180
177
|
href={url}
|
|
181
|
-
|
|
178
|
+
pending={pending}
|
|
182
179
|
{...linkProps}
|
|
183
|
-
className={cn('flex-row gap-
|
|
180
|
+
className={cn('flex-row gap-1.5 p-2 hover:shadow', linkProps?.className)}
|
|
184
181
|
>
|
|
185
182
|
{iconElement || (
|
|
186
183
|
<File
|
|
187
184
|
id={fileId}
|
|
188
185
|
{...props}
|
|
189
186
|
className={cn(
|
|
190
|
-
'size-
|
|
187
|
+
'size-5 shrink-0 text-foreground/80 group-hover:text-foreground duration-300 ease-out',
|
|
191
188
|
props?.className
|
|
192
189
|
)}
|
|
193
190
|
/>
|
|
194
191
|
)}
|
|
195
192
|
{name && (
|
|
196
|
-
<span className="font-medium line-clamp-1 break-all text-foreground/
|
|
193
|
+
<span className="font-medium line-clamp-1 break-all text-foreground/80 group-hover:text-foreground duration-300 ease-out">
|
|
197
194
|
{name}
|
|
198
195
|
</span>
|
|
199
196
|
)}
|
|
@@ -205,7 +202,7 @@ AttachmentFile.displayName = 'AttachmentFile';
|
|
|
205
202
|
|
|
206
203
|
function AttachmentFailed({
|
|
207
204
|
name = 'Unknown file',
|
|
208
|
-
|
|
205
|
+
pending = false,
|
|
209
206
|
iconElement,
|
|
210
207
|
containerProps,
|
|
211
208
|
...props
|
|
@@ -216,7 +213,7 @@ function AttachmentFailed({
|
|
|
216
213
|
<div
|
|
217
214
|
{...containerProps}
|
|
218
215
|
className={cn(
|
|
219
|
-
'relative min-h-
|
|
216
|
+
'relative min-h-8 min-w-8 w-full flex flex-row items-center justify-center gap-2 px-2 bg-red-600/10 rounded-md overflow-hidden',
|
|
220
217
|
containerProps?.className
|
|
221
218
|
)}
|
|
222
219
|
>
|
|
@@ -224,7 +221,7 @@ function AttachmentFailed({
|
|
|
224
221
|
<FileXCorner id={failedId} {...props} className={cn('size-6 shrink-0 text-red-600', props?.className)} />
|
|
225
222
|
)}
|
|
226
223
|
<span className="font-medium line-clamp-1 break-all text-red-600">{name}</span>
|
|
227
|
-
<Spinner loading={
|
|
224
|
+
<Spinner loading={pending} layout="overlay" />
|
|
228
225
|
</div>
|
|
229
226
|
);
|
|
230
227
|
}
|
|
@@ -252,6 +249,8 @@ function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
|
|
|
252
249
|
|
|
253
250
|
const Attachment = memo(AttachmentBase);
|
|
254
251
|
|
|
252
|
+
Attachment.displayName = 'Attachment';
|
|
253
|
+
|
|
255
254
|
export {
|
|
256
255
|
Attachment,
|
|
257
256
|
AttachmentLink,
|
|
@@ -7,6 +7,7 @@ import { cn } from './utils';
|
|
|
7
7
|
interface AvatarProps extends AvatarPrimitive.AvatarProps {
|
|
8
8
|
src?: string | undefined;
|
|
9
9
|
name?: string | undefined;
|
|
10
|
+
fallbackIconElement?: React.ReactNode | undefined;
|
|
10
11
|
online?: boolean | undefined;
|
|
11
12
|
presence?: PresenceStatus;
|
|
12
13
|
onlineProps?: React.ComponentProps<'div'>;
|
|
@@ -26,6 +27,7 @@ function AvatarBase(
|
|
|
26
27
|
{
|
|
27
28
|
src,
|
|
28
29
|
name = 'NA',
|
|
30
|
+
fallbackIconElement,
|
|
29
31
|
online,
|
|
30
32
|
presence,
|
|
31
33
|
className,
|
|
@@ -50,7 +52,7 @@ function AvatarBase(
|
|
|
50
52
|
{...fallbackProps}
|
|
51
53
|
className={cn('bg-muted size-full rounded-full flex items-center justify-center', fallbackProps?.className)}
|
|
52
54
|
>
|
|
53
|
-
{initials}
|
|
55
|
+
{fallbackIconElement || initials}
|
|
54
56
|
</AvatarPrimitive.Fallback>
|
|
55
57
|
{online && (
|
|
56
58
|
<div
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, memo } from 'react';
|
|
3
|
+
import { Phone, PhoneIncoming, PhoneMissed, PhoneOutgoing, type LucideProps } from 'lucide-react';
|
|
4
|
+
import { FormattedDate, type FormattedDateProps } from './formatted-date';
|
|
5
|
+
import { cn } from './utils';
|
|
6
|
+
|
|
7
|
+
interface CallMessageProps extends React.ComponentProps<'div'> {
|
|
8
|
+
signal?: 'reject' | 'notAnswer' | 'hungUp' | 'cancel' | undefined;
|
|
9
|
+
info?: string | undefined;
|
|
10
|
+
duration?: number | undefined;
|
|
11
|
+
fromMe: boolean;
|
|
12
|
+
isLast: boolean;
|
|
13
|
+
iconElement?: React.ReactNode;
|
|
14
|
+
iconProps?: LucideProps;
|
|
15
|
+
infoProps?: React.ComponentProps<'span'>;
|
|
16
|
+
formattedDateProps?: FormattedDateProps;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatDuration(seconds: number): string {
|
|
20
|
+
const h = Math.floor(seconds / 3600);
|
|
21
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
22
|
+
const s = seconds % 60;
|
|
23
|
+
const pad = (num: number) => String(num).padStart(2, '0');
|
|
24
|
+
|
|
25
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function CallMessageBase(
|
|
29
|
+
{
|
|
30
|
+
signal,
|
|
31
|
+
info,
|
|
32
|
+
duration = 0,
|
|
33
|
+
fromMe = false,
|
|
34
|
+
isLast = false,
|
|
35
|
+
iconElement,
|
|
36
|
+
iconProps,
|
|
37
|
+
infoProps,
|
|
38
|
+
formattedDateProps,
|
|
39
|
+
...props
|
|
40
|
+
}: CallMessageProps,
|
|
41
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
42
|
+
) {
|
|
43
|
+
const CallIcon =
|
|
44
|
+
signal === 'hungUp' ? Phone : fromMe ? (signal === 'reject' ? PhoneIncoming : PhoneOutgoing) : PhoneMissed;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
className={cn(
|
|
51
|
+
'flex items-center justify-center gap-2 rounded-full w-fit bg-ring/20 mx-auto px-3 py-1.5',
|
|
52
|
+
isLast ? 'my-2' : 'mt-2',
|
|
53
|
+
props?.className
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{iconElement || (
|
|
57
|
+
<CallIcon
|
|
58
|
+
{...iconProps}
|
|
59
|
+
className={cn('size-4', signal === 'hungUp' ? 'text-green-500' : 'text-red-500', iconProps?.className)}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<span
|
|
63
|
+
{...infoProps}
|
|
64
|
+
className={cn('text-sm mb-px', infoProps?.className)}
|
|
65
|
+
>{`${info}${duration ? ` - ${formatDuration(duration)}` : ''}`}</span>
|
|
66
|
+
<FormattedDate distanceToNow {...formattedDateProps} />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CallMessage = memo(forwardRef<HTMLDivElement, CallMessageProps>(CallMessageBase));
|
|
72
|
+
|
|
73
|
+
CallMessage.displayName = 'CallMessage';
|
|
74
|
+
|
|
75
|
+
export { CallMessage, type CallMessageProps };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
3
|
+
import { useInView } from 'react-intersection-observer';
|
|
4
|
+
import { Attachment, type AttachmentProps } from './attachment';
|
|
5
|
+
import { Avatar, type AvatarProps } from './avatar';
|
|
6
|
+
import { FormattedDate, type FormattedDateProps } from './formatted-date';
|
|
7
|
+
import { LinkifyText, type LinkifyTextProps } from './linkify-text';
|
|
8
|
+
import { LinkPreview, type LinkPreviewProps } from './link-preview';
|
|
9
|
+
import { StatusSent, type StatusSentProps } from './status-sent';
|
|
10
|
+
import { cn } from './utils';
|
|
11
|
+
|
|
12
|
+
interface ChatMessageProps extends React.ComponentProps<'div'> {
|
|
13
|
+
isLast: boolean;
|
|
14
|
+
fromMe: boolean;
|
|
15
|
+
sameSenderAbove: boolean;
|
|
16
|
+
title?: string;
|
|
17
|
+
senderName?: string;
|
|
18
|
+
senderAvatar?: string;
|
|
19
|
+
attachmentElement?: React.ReactNode;
|
|
20
|
+
linkifyTextElement?: React.ReactNode;
|
|
21
|
+
linkPreviewElement?: React.ReactNode;
|
|
22
|
+
onView?: () => void;
|
|
23
|
+
avatarProps?: AvatarProps;
|
|
24
|
+
bubbleProps?: React.ComponentProps<'div'>;
|
|
25
|
+
titleProps?: React.ComponentProps<'span'>;
|
|
26
|
+
formattedDateProps?: FormattedDateProps;
|
|
27
|
+
statusSentProps?: StatusSentProps;
|
|
28
|
+
attachmentProps?: AttachmentProps;
|
|
29
|
+
linkifyTextProps?: LinkifyTextProps;
|
|
30
|
+
linkPreviewProps?: LinkPreviewProps;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ChatMessageBase(
|
|
34
|
+
{
|
|
35
|
+
isLast,
|
|
36
|
+
fromMe,
|
|
37
|
+
sameSenderAbove,
|
|
38
|
+
title,
|
|
39
|
+
senderName,
|
|
40
|
+
senderAvatar,
|
|
41
|
+
attachmentElement,
|
|
42
|
+
linkifyTextElement,
|
|
43
|
+
linkPreviewElement,
|
|
44
|
+
onView = () => {},
|
|
45
|
+
avatarProps,
|
|
46
|
+
bubbleProps,
|
|
47
|
+
titleProps,
|
|
48
|
+
formattedDateProps,
|
|
49
|
+
statusSentProps,
|
|
50
|
+
attachmentProps,
|
|
51
|
+
linkifyTextProps,
|
|
52
|
+
linkPreviewProps,
|
|
53
|
+
children,
|
|
54
|
+
...props
|
|
55
|
+
}: ChatMessageProps,
|
|
56
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
57
|
+
) {
|
|
58
|
+
const [setRef, inView] = useInView();
|
|
59
|
+
const messageRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const setRefs = useCallback(
|
|
61
|
+
(node: HTMLDivElement) => {
|
|
62
|
+
messageRef.current = node;
|
|
63
|
+
setRef(node);
|
|
64
|
+
},
|
|
65
|
+
[setRef]
|
|
66
|
+
);
|
|
67
|
+
const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
|
|
68
|
+
const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
|
|
69
|
+
const hasThinMarginTop = hasAvatarMargin || Boolean(fromMe && sameSenderAbove);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (inView) {
|
|
73
|
+
onView();
|
|
74
|
+
}
|
|
75
|
+
}, [inView, onView]);
|
|
76
|
+
|
|
77
|
+
useImperativeHandle(ref, () => messageRef.current!, []);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
ref={setRefs}
|
|
82
|
+
{...props}
|
|
83
|
+
className={cn(
|
|
84
|
+
`flex relative text-left whitespace-pre-wrap`,
|
|
85
|
+
fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
|
|
86
|
+
isLast && 'mb-2',
|
|
87
|
+
hasThinMarginTop ? 'mt-1' : 'mt-2',
|
|
88
|
+
inView && 'view',
|
|
89
|
+
props?.className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{hasAvatar && (
|
|
93
|
+
<Avatar
|
|
94
|
+
name={senderName}
|
|
95
|
+
src={senderAvatar}
|
|
96
|
+
imageProps={{ className: 'bg-ring/30' }}
|
|
97
|
+
fallbackProps={{ className: 'bg-ring/30' }}
|
|
98
|
+
{...avatarProps}
|
|
99
|
+
className={cn('mt-1 mr-1', avatarProps?.className)}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div
|
|
104
|
+
className={cn(
|
|
105
|
+
'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6',
|
|
106
|
+
fromMe ? 'bg-blue-200' : 'bg-gray-200',
|
|
107
|
+
hasAvatarMargin && 'ml-9',
|
|
108
|
+
bubbleProps?.className
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{(title || senderName) && (
|
|
112
|
+
<span
|
|
113
|
+
{...titleProps}
|
|
114
|
+
className={cn(
|
|
115
|
+
'font-semibold',
|
|
116
|
+
title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
|
|
117
|
+
titleProps?.className
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{title || senderName}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
{children}
|
|
124
|
+
{attachmentElement || (attachmentProps ? <Attachment {...attachmentProps} /> : null)}
|
|
125
|
+
{linkifyTextElement || (linkifyTextProps ? <LinkifyText {...linkifyTextProps} /> : null)}
|
|
126
|
+
{linkPreviewElement || (linkPreviewProps ? <LinkPreview {...linkPreviewProps} /> : null)}
|
|
127
|
+
<div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
|
|
128
|
+
<FormattedDate distanceToNow {...formattedDateProps} />
|
|
129
|
+
<StatusSent {...statusSentProps} />
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ChatMessage = memo(forwardRef<HTMLDivElement, ChatMessageProps>(ChatMessageBase));
|
|
137
|
+
|
|
138
|
+
export { ChatMessage, type ChatMessageProps };
|