@linktr.ee/messaging-react 3.13.4 → 3.14.0-rc-1784604876

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/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-34_jzKSz.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;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-CBSdgfCs.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
@@ -1312,6 +1312,15 @@ export declare interface MessageMetadata {
1312
1312
  attachment_mime_type?: string;
1313
1313
  attachment_thumbnail?: string;
1314
1314
  attachment_detail?: string;
1315
+ /**
1316
+ * The client surface a linker send was composed from, when it needs to be
1317
+ * distinguished server-side. A plain reply carries no `sent_from`; a single
1318
+ * reply composed inside stack-detail channel mode sets `'stack_detail'` so the
1319
+ * backend can leave stack removal confirm-gated (client-owned) instead of
1320
+ * silently removing the conversation from its stack. Composes with
1321
+ * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
1322
+ */
1323
+ sent_from?: 'stack_detail';
1315
1324
  }
1316
1325
 
1317
1326
  export declare const MessageVoteButtons: default_2.FC<MessageVoteButtonsProps>;
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-Cw1ij-Py.js";
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-0uNeCMZl.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/dist/testing.d.ts CHANGED
@@ -53,6 +53,15 @@ export declare interface MessageMetadata {
53
53
  attachment_mime_type?: string;
54
54
  attachment_thumbnail?: string;
55
55
  attachment_detail?: string;
56
+ /**
57
+ * The client surface a linker send was composed from, when it needs to be
58
+ * distinguished server-side. A plain reply carries no `sent_from`; a single
59
+ * reply composed inside stack-detail channel mode sets `'stack_detail'` so the
60
+ * backend can leave stack removal confirm-gated (client-owned) instead of
61
+ * silently removing the conversation from its stack. Composes with
62
+ * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
63
+ */
64
+ sent_from?: 'stack_detail';
56
65
  }
57
66
 
58
67
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.13.4",
3
+ "version": "3.14.0-rc-1784604876",
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.2.0",
53
+ "@linktr.ee/messaging-core": "2.3.0-rc-1784604876",
54
54
  "@phosphor-icons/react": "^2.1.10"
55
55
  },
56
56
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import { renderWithProviders, screen } from '../../test/utils'
5
5
 
@@ -276,4 +276,101 @@ describe('CustomMessageInput', () => {
276
276
  expect(screen.queryByText(/should not show/i)).not.toBeInTheDocument()
277
277
  expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
278
278
  })
279
+
280
+ // MES-1303: the floating composer overlays the message list, so growing it
281
+ // reserves space via padding-bottom but never moves the viewport. The resize
282
+ // observer re-pins the thread to the latest message when the composer grows —
283
+ // but only if the user was already at the bottom.
284
+ describe('re-pins the thread when the floating composer grows (MES-1303)', () => {
285
+ // Render the floating composer inside the real layout structure so its
286
+ // `parentElement` is `.messaging-channel-layout` and it can find the Stream
287
+ // scroll container the way it does in ChannelView.
288
+ const renderInLayout = () =>
289
+ renderWithProviders(
290
+ <div className="messaging-channel-layout">
291
+ <div className="messaging-channel-message-list--floating">
292
+ <div
293
+ className="str-chat__message-list-scroll"
294
+ data-testid="scroller"
295
+ />
296
+ </div>
297
+ <CustomMessageInput
298
+ composerInput={() => <div data-testid="custom-composer" />}
299
+ />
300
+ </div>
301
+ )
302
+
303
+ // Force jsdom's zero-valued scroll geometry to a chosen shape. jsdom has no
304
+ // layout engine, so setProperty can't grow scrollHeight the way a browser
305
+ // reflow would; we set the post-growth geometry directly.
306
+ const setScrollGeometry = (
307
+ el: HTMLElement,
308
+ { scrollHeight, clientHeight, scrollTop }: Record<string, number>
309
+ ) => {
310
+ Object.defineProperty(el, 'scrollHeight', {
311
+ value: scrollHeight,
312
+ configurable: true,
313
+ })
314
+ Object.defineProperty(el, 'clientHeight', {
315
+ value: clientHeight,
316
+ configurable: true,
317
+ })
318
+ el.scrollTop = scrollTop
319
+ }
320
+
321
+ let resizeCallback: ResizeObserverCallback | undefined
322
+
323
+ beforeEach(() => {
324
+ mockChannelData = {}
325
+ resizeCallback = undefined
326
+ vi.stubGlobal(
327
+ 'ResizeObserver',
328
+ class {
329
+ constructor(cb: ResizeObserverCallback) {
330
+ resizeCallback = cb
331
+ }
332
+ observe() {}
333
+ unobserve() {}
334
+ disconnect() {}
335
+ }
336
+ )
337
+ })
338
+
339
+ afterEach(() => {
340
+ vi.unstubAllGlobals()
341
+ })
342
+
343
+ it('scrolls to the bottom when the user was already at the bottom', () => {
344
+ renderInLayout()
345
+ const scroller = screen.getByTestId('scroller')
346
+
347
+ // At the bottom: scrollHeight - scrollTop - clientHeight = 0.
348
+ setScrollGeometry(scroller, {
349
+ scrollHeight: 560,
350
+ clientHeight: 300,
351
+ scrollTop: 260,
352
+ })
353
+
354
+ resizeCallback?.([], {} as ResizeObserver)
355
+
356
+ expect(scroller.scrollTop).toBe(560)
357
+ })
358
+
359
+ it('leaves the scroll position alone when the user has scrolled up', () => {
360
+ renderInLayout()
361
+ const scroller = screen.getByTestId('scroller')
362
+
363
+ // Scrolled up reading history: distance from bottom (600px) is well over
364
+ // the threshold.
365
+ setScrollGeometry(scroller, {
366
+ scrollHeight: 1000,
367
+ clientHeight: 300,
368
+ scrollTop: 100,
369
+ })
370
+
371
+ resizeCallback?.([], {} as ResizeObserver)
372
+
373
+ expect(scroller.scrollTop).toBe(100)
374
+ })
375
+ })
279
376
  })
@@ -23,6 +23,14 @@ import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
23
23
  */
24
24
  const ComposerLockedContext = React.createContext(false)
25
25
 
26
+ /**
27
+ * Distance from the bottom (px) still treated as "pinned to the latest
28
+ * message". Small enough not to fire mid-scrollback, larger than sub-pixel
29
+ * rounding drift. Used to decide whether a growing composer should re-pin the
30
+ * thread to the bottom (MES-1303).
31
+ */
32
+ const COMPOSER_REPIN_THRESHOLD_PX = 40
33
+
26
34
  /**
27
35
  * Read the composer's locked (frozen-channel) state from inside a custom
28
36
  * composer supplied via `composerInput`. Returns true when the channel is
@@ -170,10 +178,30 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
170
178
  }
171
179
 
172
180
  const updateComposerHeight = () => {
181
+ // The floating composer overlays the message list; growing it only
182
+ // reserves space via padding-bottom, which never moves the viewport, so
183
+ // a taller composer creeps up over the last message (MES-1303). Capture
184
+ // whether the thread is pinned to the latest message *before* the padding
185
+ // grows, then re-pin afterwards. Guarded on `wasAtBottom` so we never
186
+ // yank a user who has scrolled up to read history.
187
+ const scroller = layout.querySelector<HTMLElement>(
188
+ '.str-chat__message-list-scroll'
189
+ )
190
+ const wasAtBottom = scroller
191
+ ? scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <
192
+ COMPOSER_REPIN_THRESHOLD_PX
193
+ : false
194
+
173
195
  layout.style.setProperty(
174
196
  '--messaging-composer-height',
175
197
  `${composer.offsetHeight + 16}px`
176
198
  )
199
+
200
+ // Reading scrollHeight forces a synchronous reflow, so it reflects the
201
+ // padding-bottom we just set; scrolling to it pins the true bottom.
202
+ if (wasAtBottom && scroller) {
203
+ scroller.scrollTop = scroller.scrollHeight
204
+ }
177
205
  }
178
206
 
179
207
  updateComposerHeight()
@@ -48,6 +48,15 @@ export interface MessageMetadata {
48
48
  attachment_mime_type?: string
49
49
  attachment_thumbnail?: string
50
50
  attachment_detail?: string
51
+ /**
52
+ * The client surface a linker send was composed from, when it needs to be
53
+ * distinguished server-side. A plain reply carries no `sent_from`; a single
54
+ * reply composed inside stack-detail channel mode sets `'stack_detail'` so the
55
+ * backend can leave stack removal confirm-gated (client-owned) instead of
56
+ * silently removing the conversation from its stack. Composes with
57
+ * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
58
+ */
59
+ sent_from?: 'stack_detail'
51
60
  }
52
61
 
53
62
  /**