@connectycube/react-ui-kit 0.1.0 → 0.1.2
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/README.md +2 -2
- package/gen/components/alert-dialog.jsx +2 -2
- package/gen/components/attachment.jsx +10 -11
- package/gen/components/avatar.jsx +2 -2
- package/gen/components/badge.jsx +2 -2
- package/gen/components/button.jsx +4 -3
- package/gen/components/chat-bubble.jsx +8 -8
- package/gen/components/chat-input.jsx +10 -10
- package/gen/components/chat-list.jsx +13 -13
- package/gen/components/checkbox.jsx +2 -2
- package/gen/components/dialog-item.jsx +2 -2
- package/gen/components/dialogs-list.jsx +4 -4
- package/gen/components/dismiss-layer.jsx +7 -7
- package/gen/components/file-picker.jsx +4 -4
- package/gen/components/formatted-date.jsx +2 -2
- package/gen/components/input.jsx +2 -2
- package/gen/components/label.jsx +2 -2
- package/gen/components/link-preview.jsx +7 -7
- package/gen/components/linkify-text.jsx +5 -5
- package/gen/components/placeholder-text.jsx +1 -2
- package/gen/components/presence.jsx +1 -0
- package/gen/components/quick-actions.jsx +2 -2
- package/gen/components/search.jsx +3 -3
- package/gen/components/spinner.jsx +3 -2
- package/gen/components/status-call.jsx +1 -0
- package/gen/components/status-indicator.jsx +2 -2
- package/gen/components/status-sent.jsx +1 -0
- package/gen/components/stream-view.jsx +16 -16
- package/package.json +14 -14
- package/src/components/alert-dialog.tsx +2 -3
- package/src/components/attachment.tsx +12 -13
- package/src/components/avatar.tsx +2 -3
- package/src/components/badge.tsx +2 -3
- package/src/components/button.tsx +4 -4
- package/src/components/chat-bubble.tsx +9 -10
- package/src/components/chat-input.tsx +17 -13
- package/src/components/chat-list.tsx +31 -24
- package/src/components/checkbox.tsx +2 -3
- package/src/components/dialog-item.tsx +2 -3
- package/src/components/dialogs-list.tsx +4 -5
- package/src/components/dismiss-layer.tsx +7 -8
- package/src/components/file-picker.tsx +4 -5
- package/src/components/formatted-date.tsx +4 -3
- package/src/components/input.tsx +2 -3
- package/src/components/label.tsx +2 -3
- package/src/components/link-preview.tsx +16 -26
- package/src/components/linkify-text.tsx +5 -6
- package/src/components/placeholder-text.tsx +1 -2
- package/src/components/presence.tsx +1 -1
- package/src/components/quick-actions.tsx +2 -3
- package/src/components/search.tsx +3 -4
- package/src/components/spinner.tsx +3 -2
- package/src/components/status-call.tsx +1 -0
- package/src/components/status-indicator.tsx +2 -3
- package/src/components/status-sent.tsx +1 -0
- package/src/components/stream-view.tsx +18 -17
- package/src/components/connectycube-ui/attachment.tsx +0 -269
- package/src/components/connectycube-ui/avatar.jsx +0 -54
- package/src/components/connectycube-ui/avatar.tsx +0 -77
- package/src/components/connectycube-ui/badge.jsx +0 -45
- package/src/components/connectycube-ui/badge.tsx +0 -42
- package/src/components/connectycube-ui/chat-input.tsx +0 -174
- package/src/components/connectycube-ui/chat-message.tsx +0 -138
- package/src/components/connectycube-ui/dialog-item.jsx +0 -149
- package/src/components/connectycube-ui/dialog-item.tsx +0 -188
- package/src/components/connectycube-ui/file-picker.jsx +0 -200
- package/src/components/connectycube-ui/file-picker.tsx +0 -231
- package/src/components/connectycube-ui/formatted-date.jsx +0 -57
- package/src/components/connectycube-ui/formatted-date.tsx +0 -57
- package/src/components/connectycube-ui/label.jsx +0 -22
- package/src/components/connectycube-ui/label.tsx +0 -23
- package/src/components/connectycube-ui/link-preview.tsx +0 -149
- package/src/components/connectycube-ui/linkify-text.tsx +0 -40
- package/src/components/connectycube-ui/presence.jsx +0 -81
- package/src/components/connectycube-ui/presence.tsx +0 -96
- package/src/components/connectycube-ui/status-sent.jsx +0 -21
- package/src/components/connectycube-ui/status-sent.tsx +0 -25
- package/src/components/connectycube-ui/utils.js +0 -10
- package/src/components/connectycube-ui/utils.ts +0 -10
|
@@ -1,138 +0,0 @@
|
|
|
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 };
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { forwardRef, memo } from 'react';
|
|
2
|
-
import { Users } from 'lucide-react';
|
|
3
|
-
import { cn } from './utils';
|
|
4
|
-
import { Avatar } from './avatar';
|
|
5
|
-
import { StatusSent } from './status-sent';
|
|
6
|
-
import { FormattedDate } from './formatted-date';
|
|
7
|
-
import { Badge } from './badge';
|
|
8
|
-
|
|
9
|
-
function DialogItemBase(
|
|
10
|
-
{
|
|
11
|
-
index = -1,
|
|
12
|
-
isPrivateDialog,
|
|
13
|
-
selected,
|
|
14
|
-
onSelect = () => {},
|
|
15
|
-
name,
|
|
16
|
-
photo,
|
|
17
|
-
userOnline,
|
|
18
|
-
userPresence,
|
|
19
|
-
hasGroupIcon,
|
|
20
|
-
lastSentStatus,
|
|
21
|
-
lastSentDate,
|
|
22
|
-
lastSenderName,
|
|
23
|
-
lastMessage,
|
|
24
|
-
typingStatusText,
|
|
25
|
-
draft,
|
|
26
|
-
draftLabel = 'Draft',
|
|
27
|
-
unreadCount = 0,
|
|
28
|
-
language = 'en',
|
|
29
|
-
divider = 'bottom',
|
|
30
|
-
avatarProps,
|
|
31
|
-
contentProps,
|
|
32
|
-
headerProps,
|
|
33
|
-
headerLeftProps,
|
|
34
|
-
groupIconProps,
|
|
35
|
-
nameProps,
|
|
36
|
-
headerRightProps,
|
|
37
|
-
statusSentIconProps,
|
|
38
|
-
formattedDateProps,
|
|
39
|
-
footerProps,
|
|
40
|
-
lastMessageProps,
|
|
41
|
-
draftLabelProps,
|
|
42
|
-
lastSenderNameProps,
|
|
43
|
-
unreadBadgeProps,
|
|
44
|
-
dividerProps,
|
|
45
|
-
...props
|
|
46
|
-
},
|
|
47
|
-
ref
|
|
48
|
-
) {
|
|
49
|
-
const avatarSource = photo || avatarProps?.src;
|
|
50
|
-
const avatarFallback = name || avatarProps?.name;
|
|
51
|
-
const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online : false;
|
|
52
|
-
const avatarPresence = isPrivateDialog ? userPresence || avatarProps?.presence : undefined;
|
|
53
|
-
const showTopDivider = divider === 'top' || (divider === 'both' && index === 0);
|
|
54
|
-
const showBottomDivider = divider === 'bottom' || divider === 'both';
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div
|
|
58
|
-
ref={ref}
|
|
59
|
-
{...props}
|
|
60
|
-
onClick={onSelect}
|
|
61
|
-
className={cn(
|
|
62
|
-
'flex items-start gap-2 px-2 flex-1 cursor-pointer',
|
|
63
|
-
'transition-colors duration-200 ease-linear',
|
|
64
|
-
`${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
|
|
65
|
-
props?.className
|
|
66
|
-
)}
|
|
67
|
-
>
|
|
68
|
-
<Avatar
|
|
69
|
-
src={avatarSource}
|
|
70
|
-
name={avatarFallback}
|
|
71
|
-
online={avatarOnline}
|
|
72
|
-
presence={avatarPresence}
|
|
73
|
-
{...avatarProps}
|
|
74
|
-
className={cn(`size-13 my-2 ${photo ? 'bg-ring/10' : 'bg-ring/30'}`, avatarProps?.className)}
|
|
75
|
-
/>
|
|
76
|
-
|
|
77
|
-
<div
|
|
78
|
-
{...contentProps}
|
|
79
|
-
className={cn('relative self-stretch flex-1 flex-col text-foreground', contentProps?.className)}
|
|
80
|
-
>
|
|
81
|
-
<div {...headerProps} className={cn('flex items-center justify-between gap-1', headerProps?.className)}>
|
|
82
|
-
<div {...headerLeftProps} className={cn('flex items-center gap-1', headerLeftProps?.className)}>
|
|
83
|
-
{!isPrivateDialog && hasGroupIcon && (
|
|
84
|
-
<Users
|
|
85
|
-
{...groupIconProps}
|
|
86
|
-
className={cn('text-muted-foreground size-4 min-w-4', groupIconProps?.className)}
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
<span {...nameProps} className={cn('font-medium text-left line-clamp-1', nameProps?.className)}>
|
|
90
|
-
{name}
|
|
91
|
-
</span>
|
|
92
|
-
</div>
|
|
93
|
-
<div {...headerRightProps} className={cn('flex items-center gap-1', headerRightProps?.className)}>
|
|
94
|
-
<StatusSent status={lastSentStatus} {...statusSentIconProps} />
|
|
95
|
-
<FormattedDate date={lastSentDate} language={language} {...formattedDateProps} />
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
<div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
|
|
99
|
-
<span
|
|
100
|
-
{...lastMessageProps}
|
|
101
|
-
className={cn('text-sm text-left text-muted-foreground line-clamp-2', lastMessageProps?.className)}
|
|
102
|
-
>
|
|
103
|
-
{typingStatusText ||
|
|
104
|
-
(draft ? (
|
|
105
|
-
<span>
|
|
106
|
-
<span
|
|
107
|
-
{...draftLabelProps}
|
|
108
|
-
className={cn('text-red-500 font-medium', draftLabelProps?.className)}
|
|
109
|
-
>{`${draftLabel}: `}</span>
|
|
110
|
-
{draft}
|
|
111
|
-
</span>
|
|
112
|
-
) : (
|
|
113
|
-
<span>
|
|
114
|
-
{lastSenderName && (
|
|
115
|
-
<span
|
|
116
|
-
{...lastSenderNameProps}
|
|
117
|
-
className={cn('font-semibold', lastSenderNameProps?.className)}
|
|
118
|
-
>{`${lastSenderName}: `}</span>
|
|
119
|
-
)}
|
|
120
|
-
{lastMessage}
|
|
121
|
-
</span>
|
|
122
|
-
))}
|
|
123
|
-
</span>
|
|
124
|
-
{unreadCount > 0 && (
|
|
125
|
-
<Badge
|
|
126
|
-
{...unreadBadgeProps}
|
|
127
|
-
className={cn(
|
|
128
|
-
'bg-ring text-background text-xs rounded-full p-1 h-6 min-w-6 mt-0.5',
|
|
129
|
-
unreadBadgeProps?.className
|
|
130
|
-
)}
|
|
131
|
-
>
|
|
132
|
-
{unreadCount}
|
|
133
|
-
</Badge>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
{showTopDivider && (
|
|
137
|
-
<div {...dividerProps} className={cn('absolute -top-px left-0 right-0 border-t', dividerProps?.className)} />
|
|
138
|
-
)}
|
|
139
|
-
{showBottomDivider && <div className="absolute bottom-0 left-0 right-0 border-b" />}
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const DialogItem = memo(forwardRef(DialogItemBase));
|
|
146
|
-
|
|
147
|
-
DialogItem.displayName = 'DialogItem';
|
|
148
|
-
|
|
149
|
-
export { DialogItem };
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import { forwardRef, memo } from 'react';
|
|
3
|
-
import { Users, type LucideProps } from 'lucide-react';
|
|
4
|
-
import { cn } from './utils';
|
|
5
|
-
import { Avatar, type AvatarProps } from './avatar';
|
|
6
|
-
import { type PresenceStatus } from './presence';
|
|
7
|
-
import { StatusSent, type StatusSentProps } from './status-sent';
|
|
8
|
-
import { FormattedDate, type FormattedDateProps } from './formatted-date';
|
|
9
|
-
import { Badge, type BadgeProps } from './badge';
|
|
10
|
-
|
|
11
|
-
interface DialogItemProps extends React.ComponentProps<'div'> {
|
|
12
|
-
index?: number;
|
|
13
|
-
isPrivateDialog?: boolean;
|
|
14
|
-
selected?: boolean;
|
|
15
|
-
onSelect?: () => void;
|
|
16
|
-
name: string;
|
|
17
|
-
photo?: string;
|
|
18
|
-
userOnline?: boolean;
|
|
19
|
-
userPresence?: PresenceStatus;
|
|
20
|
-
hasGroupIcon?: boolean;
|
|
21
|
-
lastSentStatus?: StatusSentProps['status'];
|
|
22
|
-
lastSentDate?: FormattedDateProps['date'];
|
|
23
|
-
lastSenderName?: string;
|
|
24
|
-
lastMessage?: string;
|
|
25
|
-
typingStatusText?: string;
|
|
26
|
-
draft?: string;
|
|
27
|
-
draftLabel?: string;
|
|
28
|
-
unreadCount?: number;
|
|
29
|
-
language?: string;
|
|
30
|
-
divider?: 'top' | 'bottom' | 'both' | 'none';
|
|
31
|
-
avatarProps?: AvatarProps;
|
|
32
|
-
contentProps?: React.ComponentProps<'div'>;
|
|
33
|
-
headerProps?: React.ComponentProps<'div'>;
|
|
34
|
-
headerLeftProps?: React.ComponentProps<'div'>;
|
|
35
|
-
groupIconProps?: LucideProps;
|
|
36
|
-
nameProps?: React.ComponentProps<'span'>;
|
|
37
|
-
headerRightProps?: React.ComponentProps<'div'>;
|
|
38
|
-
statusSentIconProps?: StatusSentProps;
|
|
39
|
-
formattedDateProps?: FormattedDateProps;
|
|
40
|
-
footerProps?: React.ComponentProps<'div'>;
|
|
41
|
-
lastMessageProps?: React.ComponentProps<'span'>;
|
|
42
|
-
draftLabelProps?: React.ComponentProps<'span'>;
|
|
43
|
-
lastSenderNameProps?: React.ComponentProps<'span'>;
|
|
44
|
-
unreadBadgeProps?: BadgeProps;
|
|
45
|
-
dividerProps?: React.ComponentProps<'div'>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function DialogItemBase(
|
|
49
|
-
{
|
|
50
|
-
index = -1,
|
|
51
|
-
isPrivateDialog,
|
|
52
|
-
selected,
|
|
53
|
-
onSelect = () => {},
|
|
54
|
-
name,
|
|
55
|
-
photo,
|
|
56
|
-
userOnline,
|
|
57
|
-
userPresence,
|
|
58
|
-
hasGroupIcon,
|
|
59
|
-
lastSentStatus,
|
|
60
|
-
lastSentDate,
|
|
61
|
-
lastSenderName,
|
|
62
|
-
lastMessage,
|
|
63
|
-
typingStatusText,
|
|
64
|
-
draft,
|
|
65
|
-
draftLabel = 'Draft',
|
|
66
|
-
unreadCount = 0,
|
|
67
|
-
language = 'en',
|
|
68
|
-
divider = 'bottom',
|
|
69
|
-
avatarProps,
|
|
70
|
-
contentProps,
|
|
71
|
-
headerProps,
|
|
72
|
-
headerLeftProps,
|
|
73
|
-
groupIconProps,
|
|
74
|
-
nameProps,
|
|
75
|
-
headerRightProps,
|
|
76
|
-
statusSentIconProps,
|
|
77
|
-
formattedDateProps,
|
|
78
|
-
footerProps,
|
|
79
|
-
lastMessageProps,
|
|
80
|
-
draftLabelProps,
|
|
81
|
-
lastSenderNameProps,
|
|
82
|
-
unreadBadgeProps,
|
|
83
|
-
dividerProps,
|
|
84
|
-
...props
|
|
85
|
-
}: DialogItemProps,
|
|
86
|
-
ref: React.ForwardedRef<HTMLDivElement>
|
|
87
|
-
) {
|
|
88
|
-
const avatarSource = photo || avatarProps?.src;
|
|
89
|
-
const avatarFallback = name || avatarProps?.name;
|
|
90
|
-
const avatarOnline = isPrivateDialog ? userOnline || avatarProps?.online : false;
|
|
91
|
-
const avatarPresence = isPrivateDialog ? userPresence || avatarProps?.presence : undefined;
|
|
92
|
-
const showTopDivider = divider === 'top' || (divider === 'both' && index === 0);
|
|
93
|
-
const showBottomDivider = divider === 'bottom' || divider === 'both';
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<div
|
|
97
|
-
ref={ref}
|
|
98
|
-
{...props}
|
|
99
|
-
onClick={onSelect}
|
|
100
|
-
className={cn(
|
|
101
|
-
'flex items-start gap-2 px-2 flex-1 cursor-pointer',
|
|
102
|
-
'transition-colors duration-200 ease-linear',
|
|
103
|
-
`${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
|
|
104
|
-
props?.className
|
|
105
|
-
)}
|
|
106
|
-
>
|
|
107
|
-
<Avatar
|
|
108
|
-
src={avatarSource}
|
|
109
|
-
name={avatarFallback}
|
|
110
|
-
online={avatarOnline}
|
|
111
|
-
presence={avatarPresence}
|
|
112
|
-
{...avatarProps}
|
|
113
|
-
className={cn(`size-13 my-2 ${photo ? 'bg-ring/10' : 'bg-ring/30'}`, avatarProps?.className)}
|
|
114
|
-
/>
|
|
115
|
-
|
|
116
|
-
<div
|
|
117
|
-
{...contentProps}
|
|
118
|
-
className={cn('relative self-stretch flex-1 flex-col text-foreground', contentProps?.className)}
|
|
119
|
-
>
|
|
120
|
-
<div {...headerProps} className={cn('flex items-center justify-between gap-1', headerProps?.className)}>
|
|
121
|
-
<div {...headerLeftProps} className={cn('flex items-center gap-1', headerLeftProps?.className)}>
|
|
122
|
-
{!isPrivateDialog && hasGroupIcon && (
|
|
123
|
-
<Users
|
|
124
|
-
{...groupIconProps}
|
|
125
|
-
className={cn('text-muted-foreground size-4 min-w-4', groupIconProps?.className)}
|
|
126
|
-
/>
|
|
127
|
-
)}
|
|
128
|
-
<span {...nameProps} className={cn('font-medium text-left line-clamp-1', nameProps?.className)}>
|
|
129
|
-
{name}
|
|
130
|
-
</span>
|
|
131
|
-
</div>
|
|
132
|
-
<div {...headerRightProps} className={cn('flex items-center gap-1', headerRightProps?.className)}>
|
|
133
|
-
<StatusSent status={lastSentStatus} {...statusSentIconProps} />
|
|
134
|
-
<FormattedDate date={lastSentDate} language={language} {...formattedDateProps} />
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
<div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
|
|
138
|
-
<span
|
|
139
|
-
{...lastMessageProps}
|
|
140
|
-
className={cn('text-sm text-left text-muted-foreground line-clamp-2', lastMessageProps?.className)}
|
|
141
|
-
>
|
|
142
|
-
{typingStatusText ||
|
|
143
|
-
(draft ? (
|
|
144
|
-
<span>
|
|
145
|
-
<span
|
|
146
|
-
{...draftLabelProps}
|
|
147
|
-
className={cn('text-red-500 font-medium', draftLabelProps?.className)}
|
|
148
|
-
>{`${draftLabel}: `}</span>
|
|
149
|
-
{draft}
|
|
150
|
-
</span>
|
|
151
|
-
) : (
|
|
152
|
-
<span>
|
|
153
|
-
{lastSenderName && (
|
|
154
|
-
<span
|
|
155
|
-
{...lastSenderNameProps}
|
|
156
|
-
className={cn('font-semibold', lastSenderNameProps?.className)}
|
|
157
|
-
>{`${lastSenderName}: `}</span>
|
|
158
|
-
)}
|
|
159
|
-
{lastMessage}
|
|
160
|
-
</span>
|
|
161
|
-
))}
|
|
162
|
-
</span>
|
|
163
|
-
{unreadCount > 0 && (
|
|
164
|
-
<Badge
|
|
165
|
-
{...unreadBadgeProps}
|
|
166
|
-
className={cn(
|
|
167
|
-
'bg-ring text-background text-xs rounded-full p-1 h-6 min-w-6 mt-0.5',
|
|
168
|
-
unreadBadgeProps?.className
|
|
169
|
-
)}
|
|
170
|
-
>
|
|
171
|
-
{unreadCount}
|
|
172
|
-
</Badge>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
{showTopDivider && (
|
|
176
|
-
<div {...dividerProps} className={cn('absolute -top-px left-0 right-0 border-t', dividerProps?.className)} />
|
|
177
|
-
)}
|
|
178
|
-
{showBottomDivider && <div className="absolute bottom-0 left-0 right-0 border-b" />}
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const DialogItem = memo(forwardRef<HTMLDivElement, DialogItemProps>(DialogItemBase));
|
|
185
|
-
|
|
186
|
-
DialogItem.displayName = 'DialogItem';
|
|
187
|
-
|
|
188
|
-
export { DialogItem };
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { forwardRef, useState } from 'react';
|
|
2
|
-
import { FilePlusCorner, Paperclip } from 'lucide-react';
|
|
3
|
-
import { Label } from './label';
|
|
4
|
-
import { cn } from './utils';
|
|
5
|
-
|
|
6
|
-
const areValidFiles = (files, accept) => {
|
|
7
|
-
if (files.length === 0) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const acceptList =
|
|
12
|
-
typeof accept === 'string' ? accept.split(/\s*,\s*|\s+/).map((type) => type.toLowerCase().trim()) : null;
|
|
13
|
-
|
|
14
|
-
if (acceptList === null || acceptList.length === 0) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return files.every((file) => {
|
|
19
|
-
const { type, size } = file;
|
|
20
|
-
|
|
21
|
-
if (typeof type !== 'string' || typeof size !== 'number' || size <= 0 || type.length === 0) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const fileMimeType = type.toLowerCase();
|
|
26
|
-
const [fileType, fileSubtype] = fileMimeType.split('/');
|
|
27
|
-
|
|
28
|
-
return acceptList.some(
|
|
29
|
-
(acceptType) =>
|
|
30
|
-
acceptType.startsWith('*') ||
|
|
31
|
-
acceptType === fileMimeType ||
|
|
32
|
-
(fileType && acceptType.endsWith('/*') && acceptType.startsWith(fileType)) ||
|
|
33
|
-
(acceptType.startsWith('.') && acceptType.slice(1) === fileSubtype)
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
const handleFiles = (fileList, accept, multiple, onSelectFile, onInvalidFile) => {
|
|
38
|
-
const files = Array.from(fileList || []).filter(Boolean);
|
|
39
|
-
|
|
40
|
-
if (files.length === 0) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const validated = areValidFiles(files, accept);
|
|
45
|
-
|
|
46
|
-
if (validated) {
|
|
47
|
-
onSelectFile(multiple ? files : files[0] ? [files[0]] : []);
|
|
48
|
-
} else {
|
|
49
|
-
onInvalidFile();
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function FilePickerInputBase(
|
|
54
|
-
{
|
|
55
|
-
onSelectFile = () => {},
|
|
56
|
-
onInvalidFile = () => {},
|
|
57
|
-
multiple = false,
|
|
58
|
-
accept = '*/*',
|
|
59
|
-
iconElement,
|
|
60
|
-
labelProps,
|
|
61
|
-
iconProps,
|
|
62
|
-
...props
|
|
63
|
-
},
|
|
64
|
-
ref
|
|
65
|
-
) {
|
|
66
|
-
const handleChange = (event) => {
|
|
67
|
-
handleFiles(event.currentTarget.files, accept, multiple, onSelectFile, onInvalidFile);
|
|
68
|
-
event.currentTarget.value = '';
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Label
|
|
73
|
-
{...labelProps}
|
|
74
|
-
htmlFor="file-uploader"
|
|
75
|
-
className={cn(
|
|
76
|
-
'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-in-out cursor-pointer',
|
|
77
|
-
labelProps?.className
|
|
78
|
-
)}
|
|
79
|
-
>
|
|
80
|
-
{iconElement || (
|
|
81
|
-
<Paperclip
|
|
82
|
-
{...iconProps}
|
|
83
|
-
className={cn(
|
|
84
|
-
'text-foreground group-hover:scale-110 transition-all duration-200 ease-in-out',
|
|
85
|
-
iconProps?.className
|
|
86
|
-
)}
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
<input
|
|
90
|
-
ref={ref}
|
|
91
|
-
id="file-uploader"
|
|
92
|
-
type="file"
|
|
93
|
-
multiple={multiple}
|
|
94
|
-
accept={accept}
|
|
95
|
-
onChange={handleChange}
|
|
96
|
-
{...props}
|
|
97
|
-
className={cn('hidden', props?.className)}
|
|
98
|
-
/>
|
|
99
|
-
</Label>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const FilePickerInput = forwardRef(FilePickerInputBase);
|
|
104
|
-
|
|
105
|
-
FilePickerInput.displayName = 'FilePickerInput';
|
|
106
|
-
|
|
107
|
-
function FilePickerDropzoneBase(
|
|
108
|
-
{
|
|
109
|
-
onSelectFile = () => {},
|
|
110
|
-
onInvalidFile = () => {},
|
|
111
|
-
multiple = false,
|
|
112
|
-
accept = '*/*',
|
|
113
|
-
children,
|
|
114
|
-
iconElement,
|
|
115
|
-
placeholder,
|
|
116
|
-
dropZoneProps,
|
|
117
|
-
placeholderContainerProps,
|
|
118
|
-
iconProps,
|
|
119
|
-
placeholderProps,
|
|
120
|
-
...props
|
|
121
|
-
},
|
|
122
|
-
ref
|
|
123
|
-
) {
|
|
124
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
125
|
-
const handleDragEvent = (event) => {
|
|
126
|
-
event.preventDefault();
|
|
127
|
-
event.stopPropagation();
|
|
128
|
-
};
|
|
129
|
-
const handleDrop = (event) => {
|
|
130
|
-
handleDragEvent(event);
|
|
131
|
-
setIsDragging(false);
|
|
132
|
-
handleFiles(event.dataTransfer.files, accept, multiple, onSelectFile, onInvalidFile);
|
|
133
|
-
event.dataTransfer.clearData();
|
|
134
|
-
};
|
|
135
|
-
const handleDragEnter = (event) => {
|
|
136
|
-
handleDragEvent(event);
|
|
137
|
-
|
|
138
|
-
if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
|
|
139
|
-
setIsDragging(true);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
const handleDragLeave = (event) => {
|
|
143
|
-
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
144
|
-
setIsDragging(false);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div
|
|
150
|
-
ref={ref}
|
|
151
|
-
onDragEnter={handleDragEnter}
|
|
152
|
-
onDragLeave={handleDragLeave}
|
|
153
|
-
onDragOver={handleDragEvent}
|
|
154
|
-
{...props}
|
|
155
|
-
className={cn('size-full relative', props?.className)}
|
|
156
|
-
>
|
|
157
|
-
{children}
|
|
158
|
-
<div
|
|
159
|
-
onDrop={handleDrop}
|
|
160
|
-
{...dropZoneProps}
|
|
161
|
-
className={cn(
|
|
162
|
-
'group absolute top-0 left-0 size-full',
|
|
163
|
-
'flex items-center justify-center',
|
|
164
|
-
'transition-all duration-300 ease-out',
|
|
165
|
-
'border-2 border-dashed border-ring bg-ring/25 rounded-md',
|
|
166
|
-
isDragging
|
|
167
|
-
? 'opacity-100 pointer-events-auto visible scale-100'
|
|
168
|
-
: 'opacity-0 pointer-events-none invisible scale-98',
|
|
169
|
-
dropZoneProps?.className
|
|
170
|
-
)}
|
|
171
|
-
>
|
|
172
|
-
<div
|
|
173
|
-
{...placeholderContainerProps}
|
|
174
|
-
className={cn(
|
|
175
|
-
'bg-muted flex flex-row items-center gap-2 rounded-full py-2 px-4 transition-all',
|
|
176
|
-
isDragging ? 'scale-100' : 'scale-90',
|
|
177
|
-
placeholderContainerProps?.className
|
|
178
|
-
)}
|
|
179
|
-
>
|
|
180
|
-
{iconElement || (
|
|
181
|
-
<FilePlusCorner
|
|
182
|
-
absoluteStrokeWidth
|
|
183
|
-
{...iconProps}
|
|
184
|
-
className={cn('text-ring size-6', iconProps?.className)}
|
|
185
|
-
/>
|
|
186
|
-
)}
|
|
187
|
-
<span {...placeholderProps} className={cn('text-ring text-md font-bold', placeholderProps?.className)}>
|
|
188
|
-
{placeholder || 'Drop files here'}
|
|
189
|
-
</span>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const FilePickerDropzone = forwardRef(FilePickerDropzoneBase);
|
|
197
|
-
|
|
198
|
-
FilePickerDropzone.displayName = 'FilePickerDropzone';
|
|
199
|
-
|
|
200
|
-
export { FilePickerInput, FilePickerDropzone, FilePickerInput as Input, FilePickerDropzone as Dropzone };
|