@linktr.ee/messaging-react 3.3.3 → 3.3.5
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/dist/{Card-B7AF5uOB.js → Card-B9QrjooN.js} +3 -3
- package/dist/{Card-B7AF5uOB.js.map → Card-B9QrjooN.js.map} +1 -1
- package/dist/{Card-0BgubwgM.cjs → Card-BRRlz4kq.cjs} +2 -2
- package/dist/{Card-0BgubwgM.cjs.map → Card-BRRlz4kq.cjs.map} +1 -1
- package/dist/{Card-DLmUSU4A.cjs → Card-C-FCwjGa.cjs} +2 -2
- package/dist/{Card-DLmUSU4A.cjs.map → Card-C-FCwjGa.cjs.map} +1 -1
- package/dist/{Card-DchJqvYq.js → Card-CVZzYmYW.js} +2 -2
- package/dist/{Card-DchJqvYq.js.map → Card-CVZzYmYW.js.map} +1 -1
- package/dist/{Card-CvBbAoUo.cjs → Card-D_oLlfPw.cjs} +2 -2
- package/dist/{Card-CvBbAoUo.cjs.map → Card-D_oLlfPw.cjs.map} +1 -1
- package/dist/{Card-DmPpcrSU.js → Card-DzjYyrie.js} +2 -2
- package/dist/{Card-DmPpcrSU.js.map → Card-DzjYyrie.js.map} +1 -1
- package/dist/{LockedThumbnail-BQjA4HaB.js → LockedThumbnail-CJfXY_Ut.js} +2 -2
- package/dist/{LockedThumbnail-BQjA4HaB.js.map → LockedThumbnail-CJfXY_Ut.js.map} +1 -1
- package/dist/{LockedThumbnail-D9fSb4N-.cjs → LockedThumbnail-Cth1yWnH.cjs} +2 -2
- package/dist/{LockedThumbnail-D9fSb4N-.cjs.map → LockedThumbnail-Cth1yWnH.cjs.map} +1 -1
- package/dist/index-CBtOPvxW.cjs +2 -0
- package/dist/index-CBtOPvxW.cjs.map +1 -0
- package/dist/{index-BcHUpyyw.js → index-D7eRkXoG.js} +535 -537
- package/dist/index-D7eRkXoG.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +26 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/AttachmentCard/AttachmentCard.stories.tsx +104 -0
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +33 -8
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +55 -47
- package/src/components/ChannelView.stories.tsx +8 -7
- package/src/components/CloseButton/CloseButton.stories.tsx +31 -0
- package/src/components/CustomDateSeparator/CustomDateSeparator.stories.tsx +33 -0
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.stories.tsx +63 -0
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +57 -0
- package/src/components/CustomLinkPreviewList/index.tsx +2 -54
- package/src/components/CustomMessage/CustomMessage.stories.tsx +3 -2
- package/src/components/CustomMessage/MessageTag.stories.tsx +4 -2
- package/src/components/MediaMessage/MediaMessage.stories.tsx +4 -2
- package/src/hooks/useChannelModerationActions.ts +32 -14
- package/src/index.ts +6 -1
- package/src/stories/decorators/storyTime.ts +31 -0
- package/src/stream-custom-data.ts +28 -0
- package/dist/index-BcHUpyyw.js.map +0 -1
- package/dist/index-DTZNltUC.cjs +0 -2
- package/dist/index-DTZNltUC.cjs.map +0 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'stream-chat'
|
|
10
10
|
import { Chat } from 'stream-chat-react'
|
|
11
11
|
|
|
12
|
+
import { hoursAgo, minutesAgo, now } from '../stories/decorators/storyTime'
|
|
12
13
|
import { mockParticipants } from '../stories/mocks'
|
|
13
14
|
|
|
14
15
|
import { ChannelView } from './ChannelView'
|
|
@@ -50,8 +51,8 @@ const createMockChannel = async (
|
|
|
50
51
|
id: 'msg-1',
|
|
51
52
|
text: 'Hey! How are you doing?',
|
|
52
53
|
type: 'regular' as const,
|
|
53
|
-
created_at:
|
|
54
|
-
updated_at:
|
|
54
|
+
created_at: hoursAgo(1),
|
|
55
|
+
updated_at: hoursAgo(1),
|
|
55
56
|
user: participant,
|
|
56
57
|
html: '<p>Hey! How are you doing?</p>',
|
|
57
58
|
attachments: [],
|
|
@@ -68,8 +69,8 @@ const createMockChannel = async (
|
|
|
68
69
|
id: 'msg-2',
|
|
69
70
|
text: "I'm doing great, thanks! How about you?",
|
|
70
71
|
type: 'regular' as const,
|
|
71
|
-
created_at:
|
|
72
|
-
updated_at:
|
|
72
|
+
created_at: minutesAgo(50),
|
|
73
|
+
updated_at: minutesAgo(50),
|
|
73
74
|
user: mockUser,
|
|
74
75
|
html: "<p>I'm doing great, thanks! How about you?</p>",
|
|
75
76
|
attachments: [],
|
|
@@ -86,8 +87,8 @@ const createMockChannel = async (
|
|
|
86
87
|
id: 'msg-3',
|
|
87
88
|
text: 'Pretty good! Just working on some exciting stuff.',
|
|
88
89
|
type: 'regular' as const,
|
|
89
|
-
created_at:
|
|
90
|
-
updated_at:
|
|
90
|
+
created_at: minutesAgo(30),
|
|
91
|
+
updated_at: minutesAgo(30),
|
|
91
92
|
user: participant,
|
|
92
93
|
html: '<p>Pretty good! Just working on some exciting stuff.</p>',
|
|
93
94
|
attachments: [],
|
|
@@ -429,7 +430,7 @@ const WithStarButtonTemplate: StoryFn<ComponentProps> = (args) => {
|
|
|
429
430
|
const member: ChannelMemberResponse = {
|
|
430
431
|
user: mockUser,
|
|
431
432
|
user_id: mockUser.id,
|
|
432
|
-
pinned_at: isStarred ?
|
|
433
|
+
pinned_at: isStarred ? now().toISOString() : null,
|
|
433
434
|
}
|
|
434
435
|
|
|
435
436
|
client.dispatchEvent({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { CloseButton } from '.'
|
|
5
|
+
|
|
6
|
+
type ComponentProps = React.ComponentProps<typeof CloseButton>
|
|
7
|
+
|
|
8
|
+
const meta: Meta<ComponentProps> = {
|
|
9
|
+
title: 'CloseButton',
|
|
10
|
+
component: CloseButton,
|
|
11
|
+
parameters: { layout: 'centered' },
|
|
12
|
+
argTypes: {
|
|
13
|
+
onClick: { action: 'clicked' },
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
const Template: StoryFn<ComponentProps> = (args) => (
|
|
19
|
+
<div className="p-12">
|
|
20
|
+
<CloseButton {...args} />
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
25
|
+
Default.args = {}
|
|
26
|
+
|
|
27
|
+
export const OnDarkSurface: StoryFn<ComponentProps> = () => (
|
|
28
|
+
<div className="bg-[#121110] p-12">
|
|
29
|
+
<CloseButton onClick={() => undefined} />
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { daysAgo, hoursAgo, now } from '../../stories/decorators/storyTime'
|
|
5
|
+
|
|
6
|
+
import { CustomDateSeparator } from '.'
|
|
7
|
+
|
|
8
|
+
type ComponentProps = React.ComponentProps<typeof CustomDateSeparator>
|
|
9
|
+
|
|
10
|
+
const meta: Meta<ComponentProps> = {
|
|
11
|
+
title: 'CustomDateSeparator',
|
|
12
|
+
component: CustomDateSeparator,
|
|
13
|
+
parameters: { layout: 'fullscreen' },
|
|
14
|
+
}
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
const Template: StoryFn<ComponentProps> = (args) => (
|
|
18
|
+
<div className="w-[640px] bg-background-primary p-6">
|
|
19
|
+
<CustomDateSeparator {...args} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const Today: StoryFn<ComponentProps> = Template.bind({})
|
|
24
|
+
Today.args = { date: hoursAgo(2) }
|
|
25
|
+
|
|
26
|
+
export const Yesterday: StoryFn<ComponentProps> = Template.bind({})
|
|
27
|
+
Yesterday.args = { date: hoursAgo(30) }
|
|
28
|
+
|
|
29
|
+
export const LastWeek: StoryFn<ComponentProps> = Template.bind({})
|
|
30
|
+
LastWeek.args = { date: daysAgo(6) }
|
|
31
|
+
|
|
32
|
+
export const Unread: StoryFn<ComponentProps> = Template.bind({})
|
|
33
|
+
Unread.args = { date: now(), unread: true }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LinkPreview, LinkPreviewStatus } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import CustomLinkPreviewCard from './CustomLinkPreviewCard'
|
|
6
|
+
|
|
7
|
+
type ComponentProps = React.ComponentProps<typeof CustomLinkPreviewCard>
|
|
8
|
+
|
|
9
|
+
const meta: Meta<ComponentProps> = {
|
|
10
|
+
title: 'CustomLinkPreviewCard',
|
|
11
|
+
component: CustomLinkPreviewCard,
|
|
12
|
+
parameters: { layout: 'centered' },
|
|
13
|
+
argTypes: {
|
|
14
|
+
onDismiss: { action: 'dismissed' },
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
export default meta
|
|
18
|
+
|
|
19
|
+
const makePreview = (overrides: Partial<LinkPreview>): LinkPreview => ({
|
|
20
|
+
og_scrape_url: 'https://example.com',
|
|
21
|
+
status: LinkPreviewStatus.LOADED,
|
|
22
|
+
...overrides,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const Template: StoryFn<ComponentProps> = (args) => (
|
|
26
|
+
<div className="p-12">
|
|
27
|
+
<CustomLinkPreviewCard {...args} />
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export const WithTitleAndImage: StoryFn<ComponentProps> = Template.bind({})
|
|
32
|
+
WithTitleAndImage.args = {
|
|
33
|
+
link: makePreview({
|
|
34
|
+
og_scrape_url: 'https://linktr.ee/example',
|
|
35
|
+
title: 'Linktree — the link in your bio',
|
|
36
|
+
image_url: '/image-thumbnail.jpg',
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const TitleOnly: StoryFn<ComponentProps> = Template.bind({})
|
|
41
|
+
TitleOnly.args = {
|
|
42
|
+
link: makePreview({
|
|
43
|
+
og_scrape_url: 'https://example.com/blog/post',
|
|
44
|
+
title: 'A long article title that explains why widgets are important',
|
|
45
|
+
}),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const UrlOnly: StoryFn<ComponentProps> = Template.bind({})
|
|
49
|
+
UrlOnly.args = {
|
|
50
|
+
link: makePreview({
|
|
51
|
+
og_scrape_url: 'https://example.com/raw',
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const VeryLongUrl: StoryFn<ComponentProps> = Template.bind({})
|
|
56
|
+
VeryLongUrl.args = {
|
|
57
|
+
link: makePreview({
|
|
58
|
+
og_scrape_url:
|
|
59
|
+
'https://example.com/very/long/path/with/many/segments/and/query/parameters?param1=value1¶m2=value2',
|
|
60
|
+
title: 'Page with an extremely long URL',
|
|
61
|
+
image_url: '/image-thumbnail.jpg',
|
|
62
|
+
}),
|
|
63
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { XIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LinkPreview } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
interface CustomLinkPreviewCardProps {
|
|
6
|
+
link: LinkPreview
|
|
7
|
+
onDismiss: (url: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
|
|
11
|
+
link,
|
|
12
|
+
onDismiss,
|
|
13
|
+
}) => {
|
|
14
|
+
const { og_scrape_url, title, image_url } = link
|
|
15
|
+
|
|
16
|
+
const handleDismissLink = (e: React.MouseEvent) => {
|
|
17
|
+
e.preventDefault()
|
|
18
|
+
onDismiss(og_scrape_url)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
href={og_scrape_url}
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
|
|
27
|
+
>
|
|
28
|
+
{image_url && (
|
|
29
|
+
<img
|
|
30
|
+
src={image_url}
|
|
31
|
+
alt={title || ''}
|
|
32
|
+
className="h-[180px] w-full rounded-[20px] object-cover"
|
|
33
|
+
/>
|
|
34
|
+
)}
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={handleDismissLink}
|
|
38
|
+
className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
|
|
39
|
+
aria-label="Close link preview"
|
|
40
|
+
>
|
|
41
|
+
<XIcon className="size-4 text-black/90" />
|
|
42
|
+
</button>
|
|
43
|
+
<div className="p-2">
|
|
44
|
+
{title && (
|
|
45
|
+
<div className="text-[14px] font-medium leading-5 text-white">
|
|
46
|
+
{title}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<div className="text-[12px] leading-4 text-white/55">
|
|
50
|
+
{og_scrape_url}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</a>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default CustomLinkPreviewCard
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { XIcon } from '@phosphor-icons/react'
|
|
2
1
|
import React from 'react'
|
|
3
2
|
import {
|
|
4
|
-
LinkPreview,
|
|
5
3
|
LinkPreviewsManager,
|
|
6
4
|
LinkPreviewsManagerState,
|
|
7
5
|
} from 'stream-chat'
|
|
8
6
|
import { useMessageComposer, useStateStore } from 'stream-chat-react'
|
|
9
7
|
|
|
8
|
+
import CustomLinkPreviewCard from './CustomLinkPreviewCard'
|
|
9
|
+
|
|
10
10
|
const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
|
|
11
11
|
linkPreviews: Array.from(state.previews.values()).filter(
|
|
12
12
|
(preview) =>
|
|
@@ -15,58 +15,6 @@ const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
|
|
|
15
15
|
),
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
interface CustomLinkPreviewCardProps {
|
|
19
|
-
link: LinkPreview
|
|
20
|
-
onDismiss: (url: string) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
|
|
24
|
-
link,
|
|
25
|
-
onDismiss,
|
|
26
|
-
}) => {
|
|
27
|
-
const { og_scrape_url, title, image_url } = link
|
|
28
|
-
|
|
29
|
-
const handleDismissLink = (e: React.MouseEvent) => {
|
|
30
|
-
e.preventDefault()
|
|
31
|
-
onDismiss(og_scrape_url)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<a
|
|
36
|
-
href={og_scrape_url}
|
|
37
|
-
target="_blank"
|
|
38
|
-
rel="noopener noreferrer"
|
|
39
|
-
className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
|
|
40
|
-
>
|
|
41
|
-
{image_url && (
|
|
42
|
-
<img
|
|
43
|
-
src={image_url}
|
|
44
|
-
alt={title || ''}
|
|
45
|
-
className="h-[180px] w-full rounded-[20px] object-cover"
|
|
46
|
-
/>
|
|
47
|
-
)}
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
onClick={handleDismissLink}
|
|
51
|
-
className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
|
|
52
|
-
aria-label="Close link preview"
|
|
53
|
-
>
|
|
54
|
-
<XIcon className="size-4 text-black/90" />
|
|
55
|
-
</button>
|
|
56
|
-
<div className="p-2">
|
|
57
|
-
{title && (
|
|
58
|
-
<div className="text-[14px] font-medium leading-5 text-white">
|
|
59
|
-
{title}
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
<div className="text-[12px] leading-4 text-white/55">
|
|
63
|
-
{og_scrape_url}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</a>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
18
|
export const CustomLinkPreviewList = () => {
|
|
71
19
|
const { linkPreviewsManager } = useMessageComposer()
|
|
72
20
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
WithComponents,
|
|
18
18
|
} from 'stream-chat-react'
|
|
19
19
|
|
|
20
|
+
import { minutesAgo } from '../../stories/decorators/storyTime'
|
|
20
21
|
import {
|
|
21
22
|
currentUserArgType,
|
|
22
23
|
StoryUser,
|
|
@@ -46,8 +47,8 @@ const createMockChannel = async (
|
|
|
46
47
|
const mockMessages = messages.map((msg, index) => ({
|
|
47
48
|
...msg,
|
|
48
49
|
type: msg.type ?? ('regular' as const),
|
|
49
|
-
created_at:
|
|
50
|
-
updated_at:
|
|
50
|
+
created_at: minutesAgo(messages.length - index),
|
|
51
|
+
updated_at: minutesAgo(messages.length - index),
|
|
51
52
|
html: `<p>${msg.text}</p>`,
|
|
52
53
|
attachments: msg.attachments ?? [],
|
|
53
54
|
latest_reactions: [],
|
|
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import { now } from '../../stories/decorators/storyTime'
|
|
6
|
+
|
|
5
7
|
import { MessageTag } from './MessageTag'
|
|
6
8
|
|
|
7
9
|
type ComponentProps = React.ComponentProps<typeof MessageTag>
|
|
@@ -28,8 +30,8 @@ const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
|
|
|
28
30
|
id: 'msg-1',
|
|
29
31
|
text: options?.text ?? 'Hello world',
|
|
30
32
|
type: 'regular',
|
|
31
|
-
created_at:
|
|
32
|
-
updated_at:
|
|
33
|
+
created_at: now(),
|
|
34
|
+
updated_at: now(),
|
|
33
35
|
metadata: options?.metadata,
|
|
34
36
|
}) as LocalMessage
|
|
35
37
|
|
|
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import type { LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import { now } from '../../stories/decorators/storyTime'
|
|
6
|
+
|
|
5
7
|
import { MediaMessage } from '.'
|
|
6
8
|
|
|
7
9
|
const meta: Meta<typeof MediaMessage> = {
|
|
@@ -20,8 +22,8 @@ const base = (overrides: Partial<LocalMessage> = {}): LocalMessage => {
|
|
|
20
22
|
id: 'msg-1',
|
|
21
23
|
text: '',
|
|
22
24
|
type: 'regular',
|
|
23
|
-
created_at:
|
|
24
|
-
updated_at:
|
|
25
|
+
created_at: now(),
|
|
26
|
+
updated_at: now(),
|
|
25
27
|
deleted_at: null,
|
|
26
28
|
pinned_at: null,
|
|
27
29
|
status: 'received',
|
|
@@ -75,33 +75,46 @@ export const useChannelModerationActions = ({
|
|
|
75
75
|
logLabel = 'useChannelModerationActions',
|
|
76
76
|
}: UseChannelModerationActionsParams): ChannelModerationActions => {
|
|
77
77
|
const { service, debug } = useMessagingContext()
|
|
78
|
+
const participantId = participant?.user?.id
|
|
79
|
+
const willLookup = Boolean(
|
|
80
|
+
enabled && showBlockParticipant && service && participantId
|
|
81
|
+
)
|
|
82
|
+
|
|
78
83
|
const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
// Tracks the participant + service the most recent lookup has completed
|
|
85
|
+
// for. Keying on both so a service swap (e.g. MessagingProvider rebuilding
|
|
86
|
+
// its StreamChatService when config/apiKey/debug changes) re-triggers the
|
|
87
|
+
// loading state for the same participant — otherwise the menu would
|
|
88
|
+
// briefly show actionable Block/Unblock against the new service with the
|
|
89
|
+
// old service's blocked result. Computing the flag at render time — rather
|
|
90
|
+
// than from a useState updated inside useEffect — also closes the brief
|
|
91
|
+
// window where the menu would render the regular (enabled) Block button
|
|
92
|
+
// before the effect flipped the disabled placeholder on.
|
|
93
|
+
const [resolvedFor, setResolvedFor] = useState<{
|
|
94
|
+
participantId: string
|
|
95
|
+
service: unknown
|
|
96
|
+
} | null>(null)
|
|
81
97
|
const [isLeaving, setIsLeaving] = useState(false)
|
|
82
98
|
const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
|
|
83
99
|
|
|
100
|
+
const isCheckingBlockedStatus =
|
|
101
|
+
willLookup &&
|
|
102
|
+
(resolvedFor?.participantId !== participantId ||
|
|
103
|
+
resolvedFor?.service !== service)
|
|
104
|
+
|
|
84
105
|
// Resolve whether the participant is blocked whenever the participant or
|
|
85
106
|
// surface visibility changes.
|
|
86
107
|
useEffect(() => {
|
|
87
108
|
// When the lookup is skipped (Block action hidden, surface disabled, or no
|
|
88
109
|
// participant), clear any stale blocked state so a previous participant's
|
|
89
110
|
// value can't leak into the next conversation.
|
|
90
|
-
if (
|
|
91
|
-
!enabled ||
|
|
92
|
-
!showBlockParticipant ||
|
|
93
|
-
!service ||
|
|
94
|
-
!participant?.user?.id
|
|
95
|
-
) {
|
|
111
|
+
if (!willLookup || !service || !participantId) {
|
|
96
112
|
setIsParticipantBlocked(false)
|
|
97
|
-
|
|
113
|
+
setResolvedFor(null)
|
|
98
114
|
return
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
let cancelled = false
|
|
102
|
-
const participantId = participant.user.id
|
|
103
|
-
|
|
104
|
-
setIsCheckingBlockedStatus(true)
|
|
105
118
|
|
|
106
119
|
void (async () => {
|
|
107
120
|
try {
|
|
@@ -117,7 +130,12 @@ export const useChannelModerationActions = ({
|
|
|
117
130
|
console.error(`[${logLabel}] Failed to check blocked status:`, error)
|
|
118
131
|
}
|
|
119
132
|
} finally {
|
|
120
|
-
|
|
133
|
+
// Mark the lookup as resolved regardless of success/failure so a
|
|
134
|
+
// rejected `getBlockedUsers()` doesn't leave the menu stuck in the
|
|
135
|
+
// disabled-spinner state. On failure the blocked flag stays at its
|
|
136
|
+
// default (false), matching the prior behavior — the user can attempt
|
|
137
|
+
// to block/unblock and the server rejects if the state is wrong.
|
|
138
|
+
if (!cancelled) setResolvedFor({ participantId, service })
|
|
121
139
|
}
|
|
122
140
|
})()
|
|
123
141
|
|
|
@@ -125,7 +143,7 @@ export const useChannelModerationActions = ({
|
|
|
125
143
|
return () => {
|
|
126
144
|
cancelled = true
|
|
127
145
|
}
|
|
128
|
-
}, [
|
|
146
|
+
}, [willLookup, service, participantId, logLabel])
|
|
129
147
|
|
|
130
148
|
const handleLeaveConversation = async () => {
|
|
131
149
|
if (isLeaving) return
|
package/src/index.ts
CHANGED
|
@@ -57,7 +57,12 @@ export type {
|
|
|
57
57
|
Participant,
|
|
58
58
|
LockedAttachmentSource,
|
|
59
59
|
} from './types'
|
|
60
|
-
export type {
|
|
60
|
+
export type {
|
|
61
|
+
MessageMetadata,
|
|
62
|
+
OfficialCtaActionKind,
|
|
63
|
+
OfficialCtaActionPayload,
|
|
64
|
+
OfficialCtaAttachment,
|
|
65
|
+
} from './stream-custom-data'
|
|
61
66
|
export type { AvatarProps } from './components/Avatar'
|
|
62
67
|
export type { ActionButtonProps } from './components/ActionButton'
|
|
63
68
|
export type {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic clock for Storybook stories.
|
|
3
|
+
*
|
|
4
|
+
* Stories that render relative timestamps ("3m ago", "yesterday") must use these
|
|
5
|
+
* helpers instead of `new Date()` / `Date.now()`, or visual diffs in Chromatic
|
|
6
|
+
* will drift on every run.
|
|
7
|
+
*
|
|
8
|
+
* FROZEN_NOW is the reference instant. Helpers below produce timestamps relative
|
|
9
|
+
* to it so the rendered output is identical regardless of when the build runs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const FROZEN_NOW = new Date('2026-01-15T12:00:00.000Z')
|
|
13
|
+
|
|
14
|
+
const MS_PER_SEC = 1000
|
|
15
|
+
const MS_PER_MIN = 60 * MS_PER_SEC
|
|
16
|
+
const MS_PER_HOUR = 60 * MS_PER_MIN
|
|
17
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR
|
|
18
|
+
|
|
19
|
+
export const now = (): Date => new Date(FROZEN_NOW)
|
|
20
|
+
|
|
21
|
+
export const secondsAgo = (n: number): Date =>
|
|
22
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_SEC)
|
|
23
|
+
|
|
24
|
+
export const minutesAgo = (n: number): Date =>
|
|
25
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_MIN)
|
|
26
|
+
|
|
27
|
+
export const hoursAgo = (n: number): Date =>
|
|
28
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_HOUR)
|
|
29
|
+
|
|
30
|
+
export const daysAgo = (n: number): Date =>
|
|
31
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_DAY)
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* to be included in the distributed types.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { Attachment } from 'stream-chat'
|
|
15
16
|
import 'stream-chat'
|
|
16
17
|
|
|
17
18
|
export type DmAgentSystemType =
|
|
@@ -45,7 +46,34 @@ export interface MessageMetadata {
|
|
|
45
46
|
attachment_detail?: string
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Action intents supported by Linktree official CTA attachments.
|
|
51
|
+
*/
|
|
52
|
+
export type OfficialCtaActionKind =
|
|
53
|
+
/** Open the existing share-to-socials flow for the current platform. */
|
|
54
|
+
| 'post_to_socials'
|
|
55
|
+
/** Navigate to an external URL when the CTA represents plain navigation. */
|
|
56
|
+
| 'open_external_url'
|
|
57
|
+
|
|
58
|
+
export interface OfficialCtaActionPayload {
|
|
59
|
+
kind?: OfficialCtaActionKind
|
|
60
|
+
label?: string
|
|
61
|
+
url?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structured attachment shape for server-authored Linktree official CTA cards.
|
|
66
|
+
*/
|
|
67
|
+
export type OfficialCtaAttachment = Attachment & {
|
|
68
|
+
action?: OfficialCtaActionPayload
|
|
69
|
+
type: 'linktree_official_cta'
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
declare module 'stream-chat' {
|
|
73
|
+
interface CustomAttachmentData {
|
|
74
|
+
action?: OfficialCtaActionPayload
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
interface CustomChannelData {
|
|
50
78
|
/**
|
|
51
79
|
* Whether the channel has at least one visitor-originated message.
|