@linktr.ee/messaging-react 3.31.0-rc-1785485102 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-2l_y20FM.js → Card-CmlaSw7i.js} +2 -2
- package/dist/{Card-2l_y20FM.js.map → Card-CmlaSw7i.js.map} +1 -1
- package/dist/{Card-Gr4HDVbn.cjs → Card-EWN07XIl.cjs} +2 -2
- package/dist/{Card-Gr4HDVbn.cjs.map → Card-EWN07XIl.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-B96WLmx6.js → index-CN3errpB.js} +3055 -3109
- package/dist/index-CN3errpB.js.map +1 -0
- package/dist/index-CcitzVoW.cjs +5 -0
- package/dist/index-CcitzVoW.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +56 -60
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/ChannelView.broadcast-name.integration.test.tsx +116 -0
- package/src/components/ChannelView.tsx +17 -3
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +15 -0
- package/src/components/CustomLinkPreviewList/index.tsx +24 -26
- package/src/components/CustomMessage/BroadcastNameContext.test.ts +43 -0
- package/src/components/CustomMessage/BroadcastNameContext.ts +95 -0
- package/src/components/CustomMessage/CustomMessage.stories.tsx +54 -22
- package/src/components/CustomMessage/CustomMessage.test.tsx +48 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +62 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +8 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -33
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -8
- package/src/components/CustomMessage/index.tsx +23 -3
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +50 -2
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +180 -2
- package/src/components/CustomMessageInput/index.tsx +143 -16
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +57 -166
- package/src/components/MessageAttachment/Audio/index.tsx +77 -71
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +75 -175
- package/src/components/MessageAttachment/index.tsx +9 -25
- package/src/components/MessageAttachment/types.ts +9 -19
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +1 -0
- package/src/styles.css +5 -120
- package/src/types.ts +11 -0
- package/src/utils/formatRelativeTime.test.ts +63 -57
- package/src/utils/formatRelativeTime.ts +21 -20
- package/dist/index-B96WLmx6.js.map +0 -1
- package/dist/index-CCtHMrmO.cjs +0 -5
- package/dist/index-CCtHMrmO.cjs.map +0 -1
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +0 -335
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-CcitzVoW.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -33,24 +33,37 @@ export declare type AttachmentSourceType = 'image' | 'audio' | 'video' | 'docume
|
|
|
33
33
|
declare interface AudioAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
34
34
|
/** Audio source URL (`mp3`, `aac`, `wav`, …). */
|
|
35
35
|
src?: string;
|
|
36
|
-
/** MIME type hint —
|
|
36
|
+
/** MIME type hint — typed onto the inline `<source>` element. */
|
|
37
37
|
mimeType?: string;
|
|
38
38
|
/**
|
|
39
|
-
* Filename — the download default name
|
|
40
|
-
*
|
|
39
|
+
* Filename — used as the download default name (consumed by the
|
|
40
|
+
* native player's kebab menu). The HTML `<audio>` element doesn't
|
|
41
|
+
* expose a built-in title slot, so we don't render the filename
|
|
42
|
+
* inside the bubble itself.
|
|
41
43
|
*/
|
|
42
44
|
filename?: string;
|
|
43
45
|
/**
|
|
44
46
|
* Stacked audio. Takes precedence over `src` when set. Each item
|
|
45
|
-
* renders its own
|
|
46
|
-
* bubble with an 8px gap
|
|
47
|
-
*
|
|
47
|
+
* renders its own native `<audio controls>` player, vertically
|
|
48
|
+
* stacked inside the same bubble with an 8px gap between players.
|
|
49
|
+
* Sent + Received only — the composer surface accepts a single
|
|
50
|
+
* attachment at a time.
|
|
48
51
|
*/
|
|
49
52
|
items?: MessageAttachmentAudioItem[];
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `<audio preload>` hint applied to every player on the bubble.
|
|
55
|
+
* When omitted, the default depends on the rendered shape:
|
|
56
|
+
*
|
|
57
|
+
* - Single audio (no `items`, or `items.length === 1`) →
|
|
58
|
+
* `'metadata'` so the native player surfaces duration
|
|
59
|
+
* immediately.
|
|
60
|
+
* - Stacked audio (`items.length > 1`) → `'none'` so a thread
|
|
61
|
+
* of voice memos doesn't fan out N parallel metadata requests
|
|
62
|
+
* on first paint.
|
|
63
|
+
*
|
|
64
|
+
* Per-track overrides live on `AudioItem.preload`.
|
|
65
|
+
*/
|
|
66
|
+
preload?: MediaPreloadMode;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
/**
|
|
@@ -74,6 +87,8 @@ export declare type BroadcastClickHandler = (broadcast: {
|
|
|
74
87
|
name?: string;
|
|
75
88
|
}) => void;
|
|
76
89
|
|
|
90
|
+
export declare type BroadcastNameResolver = (broadcastId: string) => string | undefined;
|
|
91
|
+
|
|
77
92
|
/**
|
|
78
93
|
* Build the meta line shown under the title in compact document / file
|
|
79
94
|
* attachments — `EXT · SIZE` (`PDF · 379.5 KB`). Either part is dropped
|
|
@@ -197,7 +212,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
|
|
|
197
212
|
* Props that MessagingShell passes through to ChannelView.
|
|
198
213
|
* ChannelViewProps is the source of truth for these props.
|
|
199
214
|
*/
|
|
200
|
-
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'showChannelInfo' | 'composerInput'>;
|
|
215
|
+
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'resolveBroadcastName' | 'showChannelInfo' | 'composerInput'>;
|
|
201
216
|
|
|
202
217
|
/**
|
|
203
218
|
* ChannelView component props
|
|
@@ -388,6 +403,13 @@ export declare interface ChannelViewProps {
|
|
|
388
403
|
* Hosts own navigation so this shared package never assumes an admin URL.
|
|
389
404
|
*/
|
|
390
405
|
onBroadcastClick?: BroadcastClickHandler;
|
|
406
|
+
/**
|
|
407
|
+
* Resolves the current campaign name for a broadcast label. The host must
|
|
408
|
+
* provide a new resolver reference when a returned name changes so
|
|
409
|
+
* ChannelView re-renders and publishes the new value to memoized rows. The
|
|
410
|
+
* stamped message name remains the fallback until this returns a name.
|
|
411
|
+
*/
|
|
412
|
+
resolveBroadcastName?: BroadcastNameResolver;
|
|
391
413
|
/**
|
|
392
414
|
* Passed to Stream `Channel` as `SendButton`. Required for hosts that replace
|
|
393
415
|
* the send control: `Channel` merges this into `ComponentContext` and an
|
|
@@ -529,7 +551,9 @@ export declare function formatFileSize(bytes: number): string;
|
|
|
529
551
|
|
|
530
552
|
/**
|
|
531
553
|
* Format a date - shows time for today, relative time for older messages
|
|
532
|
-
* (e.g., "Just now", "2:08 PM" for today, "Yesterday" for yesterday,
|
|
554
|
+
* (e.g., "Just now", "2:08 PM" for today, "Yesterday" for yesterday,
|
|
555
|
+
* "Mon" for 2-6 days ago, "Jul 14" for older dates this year,
|
|
556
|
+
* "Jul 14, 2025" for previous years)
|
|
533
557
|
*/
|
|
534
558
|
export declare const formatRelativeTime: (date: Date) => string;
|
|
535
559
|
|
|
@@ -961,7 +985,7 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
961
985
|
* - Pdf / Audio / File render their compact rows / players in a
|
|
962
986
|
* vertical stack with an 8px gap between rows. Each row keeps
|
|
963
987
|
* its own activation target — Pdf opens the carousel-aware
|
|
964
|
-
* `PdfViewer` at that index, Audio gets its own
|
|
988
|
+
* `PdfViewer` at that index, Audio gets its own native player,
|
|
965
989
|
* and File downloads the asset for that row.
|
|
966
990
|
*
|
|
967
991
|
* The Composer surface accepts only a single attachment at a time,
|
|
@@ -979,29 +1003,12 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
979
1003
|
* download button to the chrome; the Video viewer relies on the
|
|
980
1004
|
* native `<video controls>` download menu, and the Pdf viewer
|
|
981
1005
|
* inherits the browser's built-in PDF download. Sibling-row Download
|
|
982
|
-
* buttons on Pdf / File and the
|
|
1006
|
+
* buttons on Pdf / File and the native `<audio>` controls expose
|
|
983
1007
|
* downloads outside the viewer too. The lightbox chrome is styled
|
|
984
1008
|
* with plain CSS (`.mes-media-viewer*`) so the package's shipped
|
|
985
1009
|
* stylesheet is enough to render the viewer correctly without a
|
|
986
1010
|
* Tailwind dependency in the consumer.
|
|
987
1011
|
*
|
|
988
|
-
* # Audio requires a `<Channel>` ancestor
|
|
989
|
-
*
|
|
990
|
-
* `MessageAttachment.Audio.*` renders the waveform player card —
|
|
991
|
-
* a 32px play toggle, a 35-bar waveform with a progress fill and a
|
|
992
|
-
* native seek control, the duration, and a 32px trailing action
|
|
993
|
-
* (Remove in the composer, Download once sent). Playback is
|
|
994
|
-
* `stream-chat-react`'s pooled `AudioPlayer`, which is what supplies
|
|
995
|
-
* single-player-at-a-time across a thread, pause-on-navigation and
|
|
996
|
-
* saved-position restore across virtualized remounts.
|
|
997
|
-
*
|
|
998
|
-
* That pool is provided by `<Channel>`. Rendered outside one the card
|
|
999
|
-
* still draws in full, but its play and seek controls are disabled —
|
|
1000
|
-
* so a story or a new consumer surface must mount `<Channel>` (or
|
|
1001
|
-
* `stream-chat-react`'s `WithAudioPlayback`) for the card to be
|
|
1002
|
-
* playable. The waveform bars themselves are styled with plain CSS
|
|
1003
|
-
* (`.mes-audio-wave*`), so the shipped stylesheet is enough.
|
|
1004
|
-
*
|
|
1005
1012
|
* # Lazy-loading defaults
|
|
1006
1013
|
*
|
|
1007
1014
|
* Media attachments bake in network-friendly defaults for long chat
|
|
@@ -1022,12 +1029,13 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
1022
1029
|
* `VideoItem.preload` per item). Once the viewer is open the
|
|
1023
1030
|
* active item bumps to `preload="metadata"` so duration / the
|
|
1024
1031
|
* first frame appear immediately.
|
|
1025
|
-
* - **Audio** —
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1032
|
+
* - **Audio** — a single audio bubble defaults to
|
|
1033
|
+
* `preload="metadata"` so the native player surfaces duration
|
|
1034
|
+
* immediately. A stacked audio bubble (`items.length > 1`)
|
|
1035
|
+
* defaults to `preload="none"` for every player so a thread of
|
|
1036
|
+
* voice memos doesn't fan out N parallel metadata requests on
|
|
1037
|
+
* first paint. Override via the attachment's `preload` prop
|
|
1038
|
+
* (or `AudioItem.preload` per track).
|
|
1031
1039
|
* - **Pdf** / **File** — no inline media element to throttle; the
|
|
1032
1040
|
* bubble renders compact rows that download / open lazily on
|
|
1033
1041
|
* activation. No `loading` / `preload` props on these surfaces.
|
|
@@ -1072,40 +1080,28 @@ export declare interface MessageAttachmentAudioComposerProps extends ComposerExt
|
|
|
1072
1080
|
src?: string;
|
|
1073
1081
|
mimeType?: string;
|
|
1074
1082
|
filename?: string;
|
|
1075
|
-
/** See `
|
|
1076
|
-
|
|
1077
|
-
/** See `AudioItem.waveformData`. */
|
|
1078
|
-
waveformData?: number[];
|
|
1083
|
+
/** See `AudioAttachmentSharedProps.preload`. */
|
|
1084
|
+
preload?: MediaPreloadMode;
|
|
1079
1085
|
}
|
|
1080
1086
|
|
|
1081
1087
|
/** A single audio file inside an Audio attachment (single or stacked). */
|
|
1082
1088
|
export declare interface MessageAttachmentAudioItem {
|
|
1083
1089
|
src: string;
|
|
1084
|
-
/** MIME type hint —
|
|
1090
|
+
/** MIME type hint — typed onto the inline `<source>` element. */
|
|
1085
1091
|
mimeType?: string;
|
|
1086
1092
|
/**
|
|
1087
|
-
* Filename — the download default name
|
|
1088
|
-
*
|
|
1093
|
+
* Filename — used as the download default name (consumed by the
|
|
1094
|
+
* native player's kebab menu).
|
|
1089
1095
|
*/
|
|
1090
1096
|
filename?: string;
|
|
1091
1097
|
/**
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1098
|
+
* Per-track override for the `<audio preload>` hint. Defaults to
|
|
1099
|
+
* the parent attachment's `preload` value, which itself defaults
|
|
1100
|
+
* to `'metadata'` for a single audio (so the native player can
|
|
1101
|
+
* surface duration immediately) and `'none'` for a stacked thread
|
|
1102
|
+
* (so a fan-out of metadata requests doesn't happen on first paint).
|
|
1097
1103
|
*/
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Waveform amplitudes, each `0..1`, in track order. Resampled to the
|
|
1101
|
-
* rendered bar count, so any length is accepted. Sourced from the
|
|
1102
|
-
* Stream attachment's `waveform_data`, which the composer computes
|
|
1103
|
-
* from the local file and writes at send time.
|
|
1104
|
-
*
|
|
1105
|
-
* When omitted, the card renders a flat placeholder row: every bar at
|
|
1106
|
-
* the minimum height, with the progress fill still tracking playback.
|
|
1107
|
-
*/
|
|
1108
|
-
waveformData?: number[];
|
|
1104
|
+
preload?: MediaPreloadMode;
|
|
1109
1105
|
}
|
|
1110
1106
|
|
|
1111
1107
|
export declare type MessageAttachmentAudioReceivedProps = AudioAttachmentSharedProps;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as p, m as d, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, o as y, y as z, z as B, B as S, D as q, E as D, G as E, H as V, I as w } from "./index-
|
|
1
|
+
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as p, m as d, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, o as y, y as z, z as B, B as S, D as q, E as D, G as E, H as V, I as w } from "./index-CN3errpB.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.31.0
|
|
3
|
+
"version": "3.31.0",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@linktr.ee/component-library": "11.8.6",
|
|
53
|
-
"@linktr.ee/messaging-core": "2.
|
|
53
|
+
"@linktr.ee/messaging-core": "^2.2.0",
|
|
54
54
|
"@linktr.ee/messaging-taxonomy": "^0.5.0",
|
|
55
55
|
"@phosphor-icons/react": "^2.1.10",
|
|
56
56
|
"culori": "^4.0.2"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { act, render, screen } from '@testing-library/react'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { Channel as StreamChannel, StreamChat } from 'stream-chat'
|
|
4
|
+
import { Channel, Chat } from 'stream-chat-react'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { ChannelView } from './ChannelView'
|
|
8
|
+
|
|
9
|
+
const message = {
|
|
10
|
+
id: 'message-1',
|
|
11
|
+
cid: 'messaging:channel-1',
|
|
12
|
+
type: 'regular',
|
|
13
|
+
text: 'July class details',
|
|
14
|
+
status: 'received',
|
|
15
|
+
user: { id: 'linker-1', name: 'Linker' },
|
|
16
|
+
metadata: {
|
|
17
|
+
broadcast_id: 'broadcast-1',
|
|
18
|
+
broadcast_name: 'July 9th Class attendees',
|
|
19
|
+
},
|
|
20
|
+
attachments: [],
|
|
21
|
+
latest_reactions: [],
|
|
22
|
+
own_reactions: [],
|
|
23
|
+
reaction_counts: {},
|
|
24
|
+
reaction_scores: {},
|
|
25
|
+
reply_count: 0,
|
|
26
|
+
mentioned_users: [],
|
|
27
|
+
created_at: new Date(),
|
|
28
|
+
updated_at: new Date(),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const createChannel = () => {
|
|
32
|
+
const client = new StreamChat('mock-api-key', {
|
|
33
|
+
allowServerSideConnect: true,
|
|
34
|
+
})
|
|
35
|
+
client.userID = 'linker-1'
|
|
36
|
+
client.user = { id: 'linker-1', name: 'Linker' }
|
|
37
|
+
|
|
38
|
+
const channel = client.channel('messaging', 'channel-1')
|
|
39
|
+
channel.initialized = true
|
|
40
|
+
channel.state.messages = [message] as never
|
|
41
|
+
channel.state.members = {
|
|
42
|
+
'linker-1': {
|
|
43
|
+
user: { id: 'linker-1', name: 'Linker' },
|
|
44
|
+
user_id: 'linker-1',
|
|
45
|
+
role: 'owner',
|
|
46
|
+
},
|
|
47
|
+
'visitor-1': {
|
|
48
|
+
user: { id: 'visitor-1', name: 'Visitor' },
|
|
49
|
+
user_id: 'visitor-1',
|
|
50
|
+
role: 'member',
|
|
51
|
+
},
|
|
52
|
+
} as never
|
|
53
|
+
|
|
54
|
+
return { channel, client }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('ChannelView broadcast-name resolution', () => {
|
|
58
|
+
it('updates an existing Stream message row when the resolved name arrives', async () => {
|
|
59
|
+
const { channel, client } = createChannel()
|
|
60
|
+
const firstResolver = () => undefined
|
|
61
|
+
const secondResolver = () => 'July 9th Class absentee'
|
|
62
|
+
const mounts = vi.fn()
|
|
63
|
+
const unmounts = vi.fn()
|
|
64
|
+
|
|
65
|
+
const RowProbe = ({ children }: { children: React.ReactNode }) => {
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
mounts()
|
|
68
|
+
return unmounts
|
|
69
|
+
}, [])
|
|
70
|
+
return <>{children}</>
|
|
71
|
+
}
|
|
72
|
+
const renderMessage = (messageNode: React.ReactElement) => (
|
|
73
|
+
<RowProbe>{messageNode}</RowProbe>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
let rerender!: ReturnType<typeof render>['rerender']
|
|
77
|
+
await act(async () => {
|
|
78
|
+
rerender = render(
|
|
79
|
+
<Chat client={client}>
|
|
80
|
+
<Channel channel={channel as StreamChannel}>
|
|
81
|
+
<ChannelView
|
|
82
|
+
channel={channel as StreamChannel}
|
|
83
|
+
resolveBroadcastName={firstResolver}
|
|
84
|
+
renderMessage={renderMessage}
|
|
85
|
+
/>
|
|
86
|
+
</Channel>
|
|
87
|
+
</Chat>
|
|
88
|
+
).rerender
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
screen.getByTestId('sent-message-broadcast-status')
|
|
93
|
+
).toHaveTextContent('July 9th Class attendees')
|
|
94
|
+
expect(mounts).toHaveBeenCalledOnce()
|
|
95
|
+
|
|
96
|
+
act(() => {
|
|
97
|
+
rerender(
|
|
98
|
+
<Chat client={client}>
|
|
99
|
+
<Channel channel={channel as StreamChannel}>
|
|
100
|
+
<ChannelView
|
|
101
|
+
channel={channel as StreamChannel}
|
|
102
|
+
resolveBroadcastName={secondResolver}
|
|
103
|
+
renderMessage={renderMessage}
|
|
104
|
+
/>
|
|
105
|
+
</Channel>
|
|
106
|
+
</Chat>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(
|
|
111
|
+
screen.getByTestId('sent-message-broadcast-status')
|
|
112
|
+
).toHaveTextContent('July 9th Class absentee')
|
|
113
|
+
expect(mounts).toHaveBeenCalledOnce()
|
|
114
|
+
expect(unmounts).not.toHaveBeenCalled()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -16,6 +16,10 @@ import type { ChannelViewProps } from '../types'
|
|
|
16
16
|
import { ChannelHeaderRedesign } from './ChannelHeaderRedesign'
|
|
17
17
|
import { CustomDateSeparator } from './CustomDateSeparator'
|
|
18
18
|
import { CustomMessage } from './CustomMessage'
|
|
19
|
+
import {
|
|
20
|
+
BroadcastNameContext,
|
|
21
|
+
useBroadcastNamesById,
|
|
22
|
+
} from './CustomMessage/BroadcastNameContext'
|
|
19
23
|
import { CustomMessageActions } from './CustomMessage/CustomMessageActions'
|
|
20
24
|
import { CustomMessageInput } from './CustomMessageInput'
|
|
21
25
|
import { CustomSystemMessage } from './CustomSystemMessage'
|
|
@@ -57,6 +61,7 @@ const ChannelViewInner: React.FC<{
|
|
|
57
61
|
showChannelInfo?: boolean
|
|
58
62
|
onParticipantNameClick?: () => void
|
|
59
63
|
onBroadcastClick?: ChannelViewProps['onBroadcastClick']
|
|
64
|
+
resolveBroadcastName?: ChannelViewProps['resolveBroadcastName']
|
|
60
65
|
composerInput?: ChannelViewProps['composerInput']
|
|
61
66
|
}> = ({
|
|
62
67
|
onBack,
|
|
@@ -84,9 +89,10 @@ const ChannelViewInner: React.FC<{
|
|
|
84
89
|
showChannelInfo = true,
|
|
85
90
|
onParticipantNameClick,
|
|
86
91
|
onBroadcastClick,
|
|
92
|
+
resolveBroadcastName,
|
|
87
93
|
composerInput,
|
|
88
94
|
}) => {
|
|
89
|
-
const { channel } = useChannelStateContext()
|
|
95
|
+
const { channel, messages } = useChannelStateContext()
|
|
90
96
|
const onBroadcastClickRef = useRef(onBroadcastClick)
|
|
91
97
|
useEffect(() => {
|
|
92
98
|
onBroadcastClickRef.current = onBroadcastClick
|
|
@@ -99,6 +105,12 @@ const ChannelViewInner: React.FC<{
|
|
|
99
105
|
const messageBroadcastClick = onBroadcastClick
|
|
100
106
|
? stableOnBroadcastClick
|
|
101
107
|
: undefined
|
|
108
|
+
// Resolve outside memoized Stream rows so new campaign names publish without
|
|
109
|
+
// remounting the rows.
|
|
110
|
+
const broadcastNamesById = useBroadcastNamesById(
|
|
111
|
+
messages,
|
|
112
|
+
resolveBroadcastName
|
|
113
|
+
)
|
|
102
114
|
|
|
103
115
|
const hasVisibleMessages = channel.state.messages.some(
|
|
104
116
|
(message) => message.type !== 'deleted'
|
|
@@ -153,7 +165,7 @@ const ChannelViewInner: React.FC<{
|
|
|
153
165
|
)
|
|
154
166
|
|
|
155
167
|
return (
|
|
156
|
-
|
|
168
|
+
<BroadcastNameContext.Provider value={broadcastNamesById}>
|
|
157
169
|
<WithComponents
|
|
158
170
|
overrides={{
|
|
159
171
|
Message: MessageOverride,
|
|
@@ -265,7 +277,7 @@ const ChannelViewInner: React.FC<{
|
|
|
265
277
|
)}
|
|
266
278
|
</Window>
|
|
267
279
|
</WithComponents>
|
|
268
|
-
|
|
280
|
+
</BroadcastNameContext.Provider>
|
|
269
281
|
)
|
|
270
282
|
}
|
|
271
283
|
|
|
@@ -308,6 +320,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
308
320
|
showChannelInfo = true,
|
|
309
321
|
onParticipantNameClick,
|
|
310
322
|
onBroadcastClick,
|
|
323
|
+
resolveBroadcastName,
|
|
311
324
|
composerInput,
|
|
312
325
|
}) => {
|
|
313
326
|
// Custom send message handler that:
|
|
@@ -422,6 +435,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
422
435
|
showChannelInfo={showChannelInfo}
|
|
423
436
|
onParticipantNameClick={onParticipantNameClick}
|
|
424
437
|
onBroadcastClick={onBroadcastClick}
|
|
438
|
+
resolveBroadcastName={resolveBroadcastName}
|
|
425
439
|
composerInput={composerInput}
|
|
426
440
|
/>
|
|
427
441
|
</Channel>
|
|
@@ -64,6 +64,21 @@ describe('CustomLinkPreviewList', () => {
|
|
|
64
64
|
expect(screen.queryByRole('img')).toBeNull()
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
+
it('is not a scroller of its own: the composer card scrolls as one piece (MES-1037)', () => {
|
|
68
|
+
// The inverted layout contract — see this component's doc comment. While
|
|
69
|
+
// this list scrolled and shrank on its own, a staged preview was cropped to
|
|
70
|
+
// keep the textarea above the keyboard instead of scrolling with it.
|
|
71
|
+
const { container } = stage({
|
|
72
|
+
status: LinkPreviewStatus.LOADED,
|
|
73
|
+
title: 'Example story',
|
|
74
|
+
image_url: 'https://cdn.example.com/story.jpg',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const list = container.querySelector('.messaging-link-preview-list')
|
|
78
|
+
expect(list).not.toHaveClass('overflow-y-auto')
|
|
79
|
+
expect(list).not.toHaveClass('min-h-0')
|
|
80
|
+
})
|
|
81
|
+
|
|
67
82
|
it('is dismiss-only: the staged card never navigates', () => {
|
|
68
83
|
// The deliberate behaviour change in MES-1349 — see this component's own
|
|
69
84
|
// doc comment for why (MES-1358 §7 Q7, touch hazard). Asserted so the
|
|
@@ -52,14 +52,15 @@ export const CustomLinkPreviewList = () => {
|
|
|
52
52
|
if (!showLinkPreviews) return null
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
// Layout contract: this list is
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
55
|
+
// Layout contract: this list is NOT a scrollable or shrinkable region. It
|
|
56
|
+
// used to be the composer's one squeezable box, giving way (min-h-0) and
|
|
57
|
+
// scrolling on its own so the textarea row could stay pinned above the
|
|
58
|
+
// keyboard. That cropped the preview to make room for the input, which read
|
|
59
|
+
// as the preview being cut off; MES-1037 inverted it. The composer card
|
|
60
|
+
// (`.central-container`) is now the single capped scroll container and this
|
|
61
|
+
// list scrolls as one piece with the textarea inside it, so the preview
|
|
62
|
+
// always keeps its full height and only the visible slice of it changes.
|
|
63
|
+
// Do not reintroduce a nested scroller or a min-h-0 here.
|
|
63
64
|
//
|
|
64
65
|
// Horizontally the card does NOT shrink, deliberately. `CardShell` bakes
|
|
65
66
|
// Figma's `w-[290px]` and there is exactly one width, in the toolkit
|
|
@@ -89,12 +90,12 @@ export const CustomLinkPreviewList = () => {
|
|
|
89
90
|
// viewport. That is the whole trade: deform every narrow surface, or leave
|
|
90
91
|
// one sub-floor device class awkward.
|
|
91
92
|
//
|
|
92
|
-
// It is awkward, not broken.
|
|
93
|
-
// axis, so `overflow-x` computes to `auto` and
|
|
94
|
-
// instead of spilling; at 320px the dismiss button's right
|
|
95
|
-
// past the fold, reachable by scrolling. That residue
|
|
96
|
-
// owns the mobile layout contract, not to this component.
|
|
97
|
-
<div className="messaging-link-preview-list flex
|
|
93
|
+
// It is awkward, not broken. The card above sets only `overflow-y`, which
|
|
94
|
+
// promotes the other axis, so its `overflow-x` computes to `auto` and it
|
|
95
|
+
// scrolls sideways instead of spilling; at 320px the dismiss button's right
|
|
96
|
+
// edge lands ~6px past the fold, reachable by scrolling. That residue
|
|
97
|
+
// belongs to whoever owns the mobile layout contract, not to this component.
|
|
98
|
+
<div className="messaging-link-preview-list flex flex-col items-stretch w-full gap-2">
|
|
98
99
|
{stateLinkPreviews.map((linkPreview) => {
|
|
99
100
|
// A LOADING preview has no `image_url` yet, so keying the layout off
|
|
100
101
|
// the image alone rendered the skeleton as two bare text bars and then
|
|
@@ -104,18 +105,15 @@ export const CustomLinkPreviewList = () => {
|
|
|
104
105
|
const isLoading = LinkPreviewsManager.previewIsLoading(linkPreview)
|
|
105
106
|
|
|
106
107
|
return (
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onDismiss={() => handleDismiss(linkPreview.og_scrape_url)}
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
108
|
+
<LinkAttachment.Composer
|
|
109
|
+
key={linkPreview.og_scrape_url}
|
|
110
|
+
title={linkPreview.title}
|
|
111
|
+
url={linkPreview.og_scrape_url}
|
|
112
|
+
thumbnailUrl={linkPreview.image_url}
|
|
113
|
+
layout={isLoading || linkPreview.image_url ? 'featured' : 'classic'}
|
|
114
|
+
status={isLoading ? 'loading' : 'ready'}
|
|
115
|
+
onDismiss={() => handleDismiss(linkPreview.og_scrape_url)}
|
|
116
|
+
/>
|
|
119
117
|
)
|
|
120
118
|
})}
|
|
121
119
|
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildBroadcastNamesById } from './BroadcastNameContext'
|
|
4
|
+
|
|
5
|
+
describe('buildBroadcastNamesById', () => {
|
|
6
|
+
it('returns undefined when no resolver is provided', () => {
|
|
7
|
+
expect(
|
|
8
|
+
buildBroadcastNamesById(
|
|
9
|
+
[{ metadata: { broadcast_id: 'broadcast-1' } }],
|
|
10
|
+
undefined
|
|
11
|
+
)
|
|
12
|
+
).toBeUndefined()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('resolves each unique broadcast id once', () => {
|
|
16
|
+
const resolveBroadcastName = vi.fn((id: string) => `name-for-${id}`)
|
|
17
|
+
|
|
18
|
+
expect(
|
|
19
|
+
buildBroadcastNamesById(
|
|
20
|
+
[
|
|
21
|
+
{ metadata: { broadcast_id: 'broadcast-1' } },
|
|
22
|
+
{ metadata: { broadcast_id: 'broadcast-1' } },
|
|
23
|
+
{ metadata: { broadcast_id: 'broadcast-2' } },
|
|
24
|
+
{ metadata: {} },
|
|
25
|
+
],
|
|
26
|
+
resolveBroadcastName
|
|
27
|
+
)
|
|
28
|
+
).toEqual({
|
|
29
|
+
'broadcast-1': 'name-for-broadcast-1',
|
|
30
|
+
'broadcast-2': 'name-for-broadcast-2',
|
|
31
|
+
})
|
|
32
|
+
expect(resolveBroadcastName).toHaveBeenCalledTimes(2)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('omits ids the resolver cannot resolve yet', () => {
|
|
36
|
+
expect(
|
|
37
|
+
buildBroadcastNamesById(
|
|
38
|
+
[{ metadata: { broadcast_id: 'broadcast-1' } }],
|
|
39
|
+
() => undefined
|
|
40
|
+
)
|
|
41
|
+
).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
})
|