@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.
Files changed (42) hide show
  1. package/configs/dependencies.json +6 -0
  2. package/configs/imports.json +2 -0
  3. package/dist/index.cjs +1 -36
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +1 -35
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/attachment.d.ts +3 -3
  8. package/dist/types/components/attachment.d.ts.map +1 -1
  9. package/dist/types/components/avatar.d.ts +1 -0
  10. package/dist/types/components/avatar.d.ts.map +1 -1
  11. package/dist/types/components/badge.d.ts +1 -1
  12. package/dist/types/components/button.d.ts +2 -2
  13. package/dist/types/components/call-message.d.ts +17 -0
  14. package/dist/types/components/call-message.d.ts.map +1 -0
  15. package/dist/types/components/chat-message.d.ts +30 -0
  16. package/dist/types/components/chat-message.d.ts.map +1 -0
  17. package/dist/types/components/dialog-item.d.ts.map +1 -1
  18. package/dist/types/components/linkify-text.d.ts +6 -1
  19. package/dist/types/components/linkify-text.d.ts.map +1 -1
  20. package/dist/types/components/switch.d.ts.map +1 -1
  21. package/dist/types/index.d.ts +3 -0
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/gen/components/attachment.jsx +21 -19
  24. package/gen/components/avatar.jsx +14 -2
  25. package/gen/components/call-message.jsx +62 -0
  26. package/gen/components/chat-message.jsx +120 -0
  27. package/gen/components/dialog-item.jsx +4 -1
  28. package/gen/components/linkify-text.jsx +41 -2
  29. package/gen/components/switch.jsx +0 -2
  30. package/gen/index.js +4 -0
  31. package/package.json +11 -10
  32. package/src/components/attachment.tsx +25 -26
  33. package/src/components/avatar.tsx +3 -1
  34. package/src/components/call-message.tsx +75 -0
  35. package/src/components/chat-message.tsx +138 -0
  36. package/src/components/connectycube-ui/attachment.tsx +269 -0
  37. package/src/components/connectycube-ui/chat-message.tsx +138 -0
  38. package/src/components/connectycube-ui/link-preview.tsx +149 -0
  39. package/src/components/dialog-item.tsx +4 -1
  40. package/src/components/linkify-text.tsx +44 -3
  41. package/src/components/switch.tsx +0 -2
  42. 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.19",
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.555.0",
78
- "prompts": "^2.4.2",
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.1",
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": "^24.10.1",
92
+ "@types/node": "^25.0.1",
95
93
  "@types/react": "^19.2.7",
96
- "eslint": "^9.39.1",
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.6.2",
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.48.0"
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
- uploading?: boolean;
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, uploading = false, children, ...props }: AttachmentLinkProps,
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-12 min-w-12 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',
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={uploading} layout="overlay" />
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, uploading = false, containerProps, ...props }: AttachmentAudioProps,
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-12 min-w-12 w-full rounded-md overflow-hidden', containerProps?.className)}
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={uploading} layout="overlay" />
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, uploading = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
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={uploading} layout="overlay" />
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, uploading = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
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} uploading={uploading} {...linkProps}>
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-12 min-w-12 max-h-[360px] group-hover:scale-103 transition-transform duration-300 ease-out',
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, uploading = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
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
- uploading={uploading}
178
+ pending={pending}
182
179
  {...linkProps}
183
- className={cn('flex-row gap-2 px-2', linkProps?.className)}
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-6 shrink-0 text-foreground/85 group-hover:text-foreground duration-300 ease-out',
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/85 group-hover:text-foreground duration-300 ease-out">
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
- uploading = false,
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-12 min-w-12 w-full flex flex-row items-center justify-center gap-2 px-2 bg-red-600/10 rounded-md overflow-hidden',
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={uploading} layout="overlay" />
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 };