stream-chat-react 14.0.0-beta.2 → 14.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/{WithAudioPlayback-myzUS2m6.js → cjs/WithAudioPlayback.4a84360f.js} +4 -9
  2. package/dist/cjs/WithAudioPlayback.4a84360f.js.map +1 -0
  3. package/dist/{audioProcessing-BbOs2wMd.js → cjs/audioProcessing.56e5db9d.js} +1 -1
  4. package/dist/cjs/audioProcessing.56e5db9d.js.map +1 -0
  5. package/dist/cjs/emojis.js +1 -1
  6. package/dist/cjs/index.js +620 -225
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/mp3-encoder.js +1 -1
  9. package/dist/css/index.css +181 -26
  10. package/dist/css/index.css.map +1 -1
  11. package/dist/{WithAudioPlayback-C1hfFIcu.mjs → es/WithAudioPlayback.a3d5a2fc.mjs} +14 -19
  12. package/dist/es/WithAudioPlayback.a3d5a2fc.mjs.map +1 -0
  13. package/dist/{audioProcessing-ByEVSjGG.mjs → es/audioProcessing.21cb49e1.mjs} +1 -1
  14. package/dist/es/audioProcessing.21cb49e1.mjs.map +1 -0
  15. package/dist/es/emojis.mjs +1 -1
  16. package/dist/es/index.mjs +632 -237
  17. package/dist/es/index.mjs.map +1 -1
  18. package/dist/es/mp3-encoder.mjs +1 -1
  19. package/dist/types/components/Attachment/Giphy.d.ts.map +1 -1
  20. package/dist/types/components/Attachment/VoiceRecording.d.ts.map +1 -1
  21. package/dist/types/components/AudioPlayback/AudioPlayer.d.ts.map +1 -1
  22. package/dist/types/components/BaseImage/toBaseImageDescriptors.d.ts +1 -1
  23. package/dist/types/components/BaseImage/toBaseImageDescriptors.d.ts.map +1 -1
  24. package/dist/types/components/ChannelList/hooks/useMobileNavigation.d.ts.map +1 -1
  25. package/dist/types/components/ChannelListItem/ChannelListItemActionButtons.d.ts +3 -1
  26. package/dist/types/components/ChannelListItem/ChannelListItemActionButtons.d.ts.map +1 -1
  27. package/dist/types/components/ChannelListItem/ChannelListItemActionButtons.defaults.d.ts +6 -3
  28. package/dist/types/components/ChannelListItem/ChannelListItemActionButtons.defaults.d.ts.map +1 -1
  29. package/dist/types/components/Chat/hooks/useSplitActionSet.d.ts +5 -0
  30. package/dist/types/components/Chat/hooks/useSplitActionSet.d.ts.map +1 -1
  31. package/dist/types/components/Dialog/components/ContextMenu.d.ts +119 -3
  32. package/dist/types/components/Dialog/components/ContextMenu.d.ts.map +1 -1
  33. package/dist/types/components/Dialog/hooks/useDialog.d.ts +1 -1
  34. package/dist/types/components/Dialog/hooks/useDialog.d.ts.map +1 -1
  35. package/dist/types/components/Dialog/service/DialogAnchor.d.ts +14 -1
  36. package/dist/types/components/Dialog/service/DialogAnchor.d.ts.map +1 -1
  37. package/dist/types/components/Dialog/service/DialogManager.d.ts +14 -3
  38. package/dist/types/components/Dialog/service/DialogManager.d.ts.map +1 -1
  39. package/dist/types/components/Dialog/service/DialogPortal.d.ts.map +1 -1
  40. package/dist/types/components/Gallery/GalleryContext.d.ts +1 -1
  41. package/dist/types/components/Gallery/GalleryContext.d.ts.map +1 -1
  42. package/dist/types/components/MessageActions/MessageActions.d.ts +14 -3
  43. package/dist/types/components/MessageActions/MessageActions.d.ts.map +1 -1
  44. package/dist/types/components/MessageActions/MessageActions.defaults.d.ts +1 -1
  45. package/dist/types/components/MessageActions/MessageActions.defaults.d.ts.map +1 -1
  46. package/dist/types/components/MessageActions/QuickMessageActionButton.d.ts.map +1 -1
  47. package/dist/types/components/MessageActions/hooks/useBaseMessageActionSetFilter.d.ts.map +1 -1
  48. package/dist/types/components/MessageComposer/AttachmentSelector/AttachmentSelector.d.ts.map +1 -1
  49. package/dist/types/components/MessageList/hooks/MessageList/useScrollLocationLogic.d.ts +18 -0
  50. package/dist/types/components/MessageList/hooks/MessageList/useScrollLocationLogic.d.ts.map +1 -1
  51. package/dist/types/components/Poll/PollActions/PollResults/PollOptionWithVotes.d.ts.map +1 -1
  52. package/dist/types/components/Reactions/MessageReactions.d.ts.map +1 -1
  53. package/dist/types/components/Reactions/MessageReactionsDetail.d.ts +1 -0
  54. package/dist/types/components/Reactions/MessageReactionsDetail.d.ts.map +1 -1
  55. package/dist/types/components/Reactions/ReactionSelector.d.ts +1 -1
  56. package/dist/types/components/Reactions/ReactionSelector.d.ts.map +1 -1
  57. package/dist/types/components/TextareaComposer/SuggestionList/SuggestionList.d.ts.map +1 -1
  58. package/dist/types/components/TextareaComposer/TextareaComposer.d.ts.map +1 -1
  59. package/dist/types/context/ChannelListContext.d.ts +1 -1
  60. package/dist/types/context/ChannelListContext.d.ts.map +1 -1
  61. package/dist/types/context/ComponentContext.d.ts +5 -1
  62. package/dist/types/context/ComponentContext.d.ts.map +1 -1
  63. package/dist/types/context/DialogManagerContext.d.ts +11 -7
  64. package/dist/types/context/DialogManagerContext.d.ts.map +1 -1
  65. package/package.json +3 -1
  66. package/dist/WithAudioPlayback-C1hfFIcu.mjs.map +0 -1
  67. package/dist/WithAudioPlayback-myzUS2m6.js.map +0 -1
  68. package/dist/audioProcessing-BbOs2wMd.js.map +0 -1
  69. package/dist/audioProcessing-ByEVSjGG.mjs.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -4,8 +4,8 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const clsx = require("clsx");
5
5
  const nanoid = require("nanoid");
6
6
  const React = require("react");
7
- const audioProcessing = require("../audioProcessing-BbOs2wMd.js");
8
- const WithAudioPlayback = require("../WithAudioPlayback-myzUS2m6.js");
7
+ const audioProcessing = require("./audioProcessing.56e5db9d.js");
8
+ const WithAudioPlayback = require("./WithAudioPlayback.4a84360f.js");
9
9
  const streamChat = require("stream-chat");
10
10
  const throttle = require("lodash.throttle");
11
11
  const linkify = require("linkifyjs");
@@ -19,7 +19,6 @@ const unistUtilVisit = require("unist-util-visit");
19
19
  const i18n = require("i18next");
20
20
  const Dayjs = require("dayjs");
21
21
  const uniqBy = require("lodash.uniqby");
22
- const tsPattern = require("ts-pattern");
23
22
  const shim = require("use-sync-external-store/shim");
24
23
  const debounce = require("lodash.debounce");
25
24
  const fixWebmDuration = require("fix-webm-duration");
@@ -89,10 +88,11 @@ const useAIState = (channel) => {
89
88
  return { aiState };
90
89
  };
91
90
  class DialogManager {
92
- constructor({ id } = {}) {
91
+ constructor({ closeOnClickOutside = true, id } = {}) {
93
92
  this.state = new streamChat.StateStore({
94
93
  dialogsById: {}
95
94
  });
95
+ this.closeOnClickOutside = closeOnClickOutside;
96
96
  this.id = id ?? nanoid.nanoid();
97
97
  }
98
98
  get openDialogCount() {
@@ -107,13 +107,14 @@ class DialogManager {
107
107
  get(id) {
108
108
  return this.state.getLatestValue().dialogsById[id];
109
109
  }
110
- getOrCreate({ id }) {
110
+ getOrCreate({ closeOnClickOutside, id }) {
111
111
  let dialog = this.state.getLatestValue().dialogsById[id];
112
112
  if (!dialog) {
113
113
  dialog = {
114
114
  close: () => {
115
115
  this.close(id);
116
116
  },
117
+ closeOnClickOutside,
117
118
  id,
118
119
  isOpen: false,
119
120
  open: () => {
@@ -129,21 +130,22 @@ class DialogManager {
129
130
  };
130
131
  this.state.next((current) => ({
131
132
  ...current,
132
- ...{ dialogsById: { ...current.dialogsById, [id]: dialog } }
133
+ dialogsById: { ...current.dialogsById, [id]: dialog }
133
134
  }));
134
135
  }
135
- if (dialog.removalTimeout) {
136
- clearTimeout(dialog.removalTimeout);
136
+ const shouldUpdateDialogSettings = dialog.closeOnClickOutside !== closeOnClickOutside || !!dialog.removalTimeout;
137
+ if (shouldUpdateDialogSettings) {
138
+ if (dialog.removalTimeout) clearTimeout(dialog.removalTimeout);
139
+ dialog = {
140
+ ...dialog,
141
+ closeOnClickOutside,
142
+ removalTimeout: void 0
143
+ };
137
144
  this.state.next((current) => ({
138
145
  ...current,
139
- ...{
140
- dialogsById: {
141
- ...current.dialogsById,
142
- [id]: {
143
- ...dialog,
144
- removalTimeout: void 0
145
- }
146
- }
146
+ dialogsById: {
147
+ ...current.dialogsById,
148
+ [id]: dialog
147
149
  }
148
150
  }));
149
151
  }
@@ -220,7 +222,11 @@ class DialogManager {
220
222
  }));
221
223
  }
222
224
  }
223
- const useDialog = ({ dialogManagerId, id }) => {
225
+ const useDialog = ({
226
+ closeOnClickOutside,
227
+ dialogManagerId,
228
+ id
229
+ }) => {
224
230
  const { dialogManager } = useDialogManager({ dialogManagerId });
225
231
  React.useEffect(
226
232
  () => () => {
@@ -228,7 +234,7 @@ const useDialog = ({ dialogManagerId, id }) => {
228
234
  },
229
235
  [dialogManager, id]
230
236
  );
231
- return dialogManager.getOrCreate({ id });
237
+ return dialogManager.getOrCreate({ closeOnClickOutside, id });
232
238
  };
233
239
  const useDialogOnNearestManager = ({ id }) => {
234
240
  const { dialogManager } = useNearestDialogManagerContext() ?? {};
@@ -275,9 +281,37 @@ const Portal = ({
275
281
  if (!portalDestination) return null;
276
282
  return reactDom.createPortal(children, portalDestination);
277
283
  };
284
+ const shouldCloseOnOutsideClick = ({
285
+ dialog,
286
+ managerCloseOnClickOutside
287
+ }) => dialog.closeOnClickOutside ?? managerCloseOnClickOutside;
278
288
  const DialogPortalDestination = () => {
279
289
  const { dialogManager } = useNearestDialogManagerContext() ?? {};
280
290
  const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
291
+ const [destinationRoot, setDestinationRoot] = React.useState(null);
292
+ React.useEffect(() => {
293
+ if (!destinationRoot || !dialogManager) return;
294
+ const handleDocumentClick = (event) => {
295
+ if (destinationRoot.contains(event.target)) return;
296
+ setTimeout(() => {
297
+ Object.values(dialogManager.state.getLatestValue().dialogsById).forEach(
298
+ (dialog) => {
299
+ if (!dialog.isOpen) return;
300
+ if (!shouldCloseOnOutsideClick({
301
+ dialog,
302
+ managerCloseOnClickOutside: dialogManager.closeOnClickOutside
303
+ }))
304
+ return;
305
+ dialogManager.close(dialog.id);
306
+ }
307
+ );
308
+ }, 0);
309
+ };
310
+ document.addEventListener("click", handleDocumentClick, { capture: true });
311
+ return () => {
312
+ document.removeEventListener("click", handleDocumentClick, { capture: true });
313
+ };
314
+ }, [destinationRoot, dialogManager]);
281
315
  if (!openedDialogCount) return null;
282
316
  return /* @__PURE__ */ jsxRuntime.jsx(
283
317
  "div",
@@ -285,7 +319,22 @@ const DialogPortalDestination = () => {
285
319
  className: "str-chat__dialog-overlay",
286
320
  "data-str-chat__portal-id": dialogManager?.id,
287
321
  "data-testid": "str-chat__dialog-overlay",
288
- onClick: () => dialogManager?.closeAll(),
322
+ onClick: (event) => {
323
+ if (!dialogManager) return;
324
+ if (event.target !== event.currentTarget) return;
325
+ Object.values(dialogManager.state.getLatestValue().dialogsById).forEach(
326
+ (dialog) => {
327
+ if (!dialog.isOpen) return;
328
+ if (!shouldCloseOnOutsideClick({
329
+ dialog,
330
+ managerCloseOnClickOutside: dialogManager.closeOnClickOutside
331
+ }))
332
+ return;
333
+ dialogManager.close(dialog.id);
334
+ }
335
+ );
336
+ },
337
+ ref: setDestinationRoot,
289
338
  style: {
290
339
  "--str-chat__dialog-overlay-height": openedDialogCount > 0 ? "100%" : "0"
291
340
  }
@@ -307,11 +356,16 @@ const DialogPortalEntry = ({
307
356
  };
308
357
  const dialogManagersRegistry = new streamChat.StateStore({});
309
358
  const getDialogManager = (id) => dialogManagersRegistry.getLatestValue()[id];
310
- const getOrCreateDialogManager = (id) => {
359
+ const getOrCreateDialogManager = ({
360
+ closeOnClickOutside,
361
+ id
362
+ }) => {
311
363
  let manager = getDialogManager(id);
312
364
  if (!manager) {
313
- manager = new DialogManager({ id });
365
+ manager = new DialogManager({ closeOnClickOutside, id });
314
366
  dialogManagersRegistry.partialNext({ [id]: manager });
367
+ } else if (typeof closeOnClickOutside === "boolean") {
368
+ manager.closeOnClickOutside = closeOnClickOutside;
315
369
  }
316
370
  return manager;
317
371
  };
@@ -322,20 +376,21 @@ const removeDialogManager = (id) => {
322
376
  const DialogManagerProviderContext = React.createContext(void 0);
323
377
  const DialogManagerProvider = ({
324
378
  children,
379
+ closeOnClickOutside,
325
380
  id
326
381
  }) => {
327
382
  const [dialogManager, setDialogManager] = React.useState(() => {
328
383
  if (id) return getDialogManager(id) ?? null;
329
- return new DialogManager();
384
+ return new DialogManager({ closeOnClickOutside });
330
385
  });
331
386
  React.useEffect(() => {
332
387
  if (!id) return;
333
- setDialogManager(getOrCreateDialogManager(id));
388
+ setDialogManager(getOrCreateDialogManager({ closeOnClickOutside, id }));
334
389
  return () => {
335
390
  removeDialogManager(id);
336
391
  setDialogManager(null);
337
392
  };
338
- }, [id]);
393
+ }, [closeOnClickOutside, id]);
339
394
  if (!dialogManager) return null;
340
395
  return /* @__PURE__ */ jsxRuntime.jsxs(DialogManagerProviderContext.Provider, { value: { dialogManager }, children: [
341
396
  children,
@@ -9054,6 +9109,7 @@ function useDialogAnchor({
9054
9109
  setPopperElement(null);
9055
9110
  }
9056
9111
  return {
9112
+ placement: stabilisedChosenPlacement ?? chosenPlacement,
9057
9113
  setPopperElement,
9058
9114
  styles: {
9059
9115
  left: x ?? 0,
@@ -9066,6 +9122,8 @@ const DialogAnchor = ({
9066
9122
  allowFlip = true,
9067
9123
  children,
9068
9124
  className,
9125
+ closeOnClickOutside,
9126
+ closeTransitionMs = 0,
9069
9127
  dialogManagerId,
9070
9128
  focus: focus$1 = true,
9071
9129
  id,
@@ -9078,12 +9136,44 @@ const DialogAnchor = ({
9078
9136
  updatePositionOnContentResize,
9079
9137
  ...restDivProps
9080
9138
  }) => {
9081
- const dialog = useDialog({ dialogManagerId, id });
9139
+ const dialog = useDialog({ closeOnClickOutside, dialogManagerId, id });
9082
9140
  const open = useDialogIsOpen(id, dialogManagerId);
9083
- const { setPopperElement, styles } = useDialogAnchor({
9141
+ const [shouldRender, setShouldRender] = React.useState(open);
9142
+ const closeTimeoutRef = React.useRef(null);
9143
+ const isClosing = !open && shouldRender;
9144
+ React.useEffect(() => {
9145
+ if (open) {
9146
+ setShouldRender(true);
9147
+ if (closeTimeoutRef.current) {
9148
+ clearTimeout(closeTimeoutRef.current);
9149
+ closeTimeoutRef.current = null;
9150
+ }
9151
+ return;
9152
+ }
9153
+ if (!shouldRender) return;
9154
+ if (!closeTransitionMs) {
9155
+ setShouldRender(false);
9156
+ return;
9157
+ }
9158
+ closeTimeoutRef.current = setTimeout(() => {
9159
+ setShouldRender(false);
9160
+ closeTimeoutRef.current = null;
9161
+ }, closeTransitionMs);
9162
+ }, [closeTransitionMs, open, shouldRender]);
9163
+ React.useEffect(
9164
+ () => () => {
9165
+ if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
9166
+ },
9167
+ []
9168
+ );
9169
+ const {
9170
+ placement: chosenPlacement,
9171
+ setPopperElement,
9172
+ styles
9173
+ } = useDialogAnchor({
9084
9174
  allowFlip,
9085
9175
  offset,
9086
- open,
9176
+ open: shouldRender,
9087
9177
  placement,
9088
9178
  referenceElement,
9089
9179
  updateKey,
@@ -9100,7 +9190,7 @@ const DialogAnchor = ({
9100
9190
  document.removeEventListener("keyup", hideOnEscape);
9101
9191
  };
9102
9192
  }, [dialog, open]);
9103
- if (!open) {
9193
+ if (!shouldRender) {
9104
9194
  return null;
9105
9195
  }
9106
9196
  return /* @__PURE__ */ jsxRuntime.jsx(DialogPortalEntry, { dialogId: id, dialogManagerId, children: /* @__PURE__ */ jsxRuntime.jsx(focus.FocusScope, { autoFocus: focus$1, contain: trapFocus, restoreFocus: true, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -9108,6 +9198,8 @@ const DialogAnchor = ({
9108
9198
  {
9109
9199
  ...restDivProps,
9110
9200
  className: clsx("str-chat__dialog-contents", className),
9201
+ "data-str-chat-dialog-state": isClosing ? "closing" : "open",
9202
+ "data-str-chat-placement": chosenPlacement,
9111
9203
  "data-testid": "str-chat__dialog-contents",
9112
9204
  ref: setPopperElement,
9113
9205
  style: styles,
@@ -9450,16 +9542,152 @@ const EmojiContextMenuButton = ({
9450
9542
  ]
9451
9543
  }
9452
9544
  );
9453
- const ContextMenuButton = ({
9454
- onBlur,
9455
- onFocus,
9456
- ...props
9545
+ const ContextMenuButtonWithSubmenu = ({
9546
+ children,
9547
+ className,
9548
+ Submenu,
9549
+ submenuContainerProps,
9550
+ submenuPlacement = "right-start",
9551
+ submenuRollAxis = "x",
9552
+ ...buttonProps
9457
9553
  }) => {
9554
+ const { className: submenuClassName, ...submenuContainerRestProps } = submenuContainerProps ?? {};
9555
+ const buttonRef = React.useRef(null);
9556
+ const [dialogContainer, setDialogContainer] = React.useState(null);
9557
+ const keepSubmenuOpenFlag = React.useRef(false);
9558
+ const dialogCloseTimeout = React.useRef(null);
9559
+ const dialogId2 = React.useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []);
9560
+ const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId2 });
9561
+ const dialogIsOpen = useDialogIsOpen(dialogId2, dialogManager?.id);
9562
+ const {
9563
+ placement: chosenPlacement,
9564
+ setPopperElement,
9565
+ styles
9566
+ } = useDialogAnchor({
9567
+ open: dialogIsOpen,
9568
+ placement: submenuPlacement,
9569
+ referenceElement: buttonRef.current
9570
+ });
9571
+ const closeDialogLazily = React.useCallback(() => {
9572
+ if (dialogCloseTimeout.current) clearTimeout(dialogCloseTimeout.current);
9573
+ dialogCloseTimeout.current = setTimeout(() => {
9574
+ if (keepSubmenuOpenFlag.current) return;
9575
+ dialog.close();
9576
+ }, 100);
9577
+ }, [dialog]);
9578
+ const keepSubmenuOpen = React.useCallback(() => {
9579
+ keepSubmenuOpenFlag.current = true;
9580
+ }, []);
9581
+ const allowToCloseSubmenu = React.useCallback(() => {
9582
+ keepSubmenuOpenFlag.current = false;
9583
+ }, []);
9584
+ const closeSubmenu = React.useCallback(() => {
9585
+ allowToCloseSubmenu();
9586
+ closeDialogLazily();
9587
+ }, [allowToCloseSubmenu, closeDialogLazily]);
9588
+ const handleClose = React.useCallback(
9589
+ (event) => {
9590
+ const parentButton = buttonRef.current;
9591
+ if (!dialogIsOpen || !parentButton) return;
9592
+ event.stopPropagation();
9593
+ closeDialogLazily();
9594
+ parentButton.focus();
9595
+ },
9596
+ [closeDialogLazily, dialogIsOpen, buttonRef]
9597
+ );
9598
+ const handleFocusParentButton = () => {
9599
+ if (dialogIsOpen) return;
9600
+ dialog.open();
9601
+ keepSubmenuOpen();
9602
+ };
9603
+ React.useEffect(() => {
9604
+ const parentButton = buttonRef.current;
9605
+ if (!dialogIsOpen || !parentButton) return;
9606
+ const hideOnEscape = (event) => {
9607
+ if (event.key !== "Escape") return;
9608
+ handleClose(event);
9609
+ closeSubmenu();
9610
+ };
9611
+ document.addEventListener("keyup", hideOnEscape, { capture: true });
9612
+ return () => {
9613
+ document.removeEventListener("keyup", hideOnEscape, { capture: true });
9614
+ };
9615
+ }, [dialogIsOpen, handleClose, closeSubmenu]);
9616
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9617
+ /* @__PURE__ */ jsxRuntime.jsx(
9618
+ BaseContextMenuButton,
9619
+ {
9620
+ "aria-selected": "false",
9621
+ className: clsx(className, "str_chat__button-with-submenu", {
9622
+ "str_chat__button-with-submenu--submenu-open": dialogIsOpen
9623
+ }),
9624
+ hasSubMenu: true,
9625
+ onBlur: closeSubmenu,
9626
+ onClick: (event) => {
9627
+ event.stopPropagation();
9628
+ dialog.toggle();
9629
+ },
9630
+ onFocus: handleFocusParentButton,
9631
+ onMouseEnter: handleFocusParentButton,
9632
+ onMouseLeave: closeSubmenu,
9633
+ role: "option",
9634
+ ...buttonProps,
9635
+ ref: buttonRef,
9636
+ children
9637
+ }
9638
+ ),
9639
+ dialogIsOpen && /* @__PURE__ */ jsxRuntime.jsx(
9640
+ "div",
9641
+ {
9642
+ className: clsx("str-chat__context-menu__submenu-container", submenuClassName),
9643
+ "data-str-chat-placement": chosenPlacement,
9644
+ "data-str-chat-roll-axis": submenuRollAxis,
9645
+ onBlur: (event) => {
9646
+ const isBlurredDescendant = event.relatedTarget instanceof Node && dialogContainer?.contains(event.relatedTarget);
9647
+ if (isBlurredDescendant) return;
9648
+ closeSubmenu();
9649
+ },
9650
+ onFocus: keepSubmenuOpen,
9651
+ onMouseEnter: keepSubmenuOpen,
9652
+ onMouseLeave: closeSubmenu,
9653
+ ref: (element) => {
9654
+ setPopperElement(element);
9655
+ setDialogContainer(element);
9656
+ },
9657
+ style: styles,
9658
+ tabIndex: -1,
9659
+ ...submenuContainerRestProps,
9660
+ children: /* @__PURE__ */ jsxRuntime.jsx(Submenu, {})
9661
+ }
9662
+ )
9663
+ ] });
9664
+ };
9665
+ const ContextMenuButton = (props) => {
9666
+ const {
9667
+ Submenu,
9668
+ submenuContainerProps,
9669
+ submenuPlacement,
9670
+ submenuRollAxis,
9671
+ ...buttonProps
9672
+ } = props;
9458
9673
  const [isFocused, setIsFocused] = React.useState(false);
9674
+ if (Submenu) {
9675
+ return /* @__PURE__ */ jsxRuntime.jsx(
9676
+ ContextMenuButtonWithSubmenu,
9677
+ {
9678
+ ...buttonProps,
9679
+ Submenu,
9680
+ submenuContainerProps,
9681
+ submenuPlacement,
9682
+ submenuRollAxis
9683
+ }
9684
+ );
9685
+ }
9686
+ const { onBlur, onFocus, ...baseButtonProps } = buttonProps;
9459
9687
  return /* @__PURE__ */ jsxRuntime.jsx(
9460
9688
  BaseContextMenuButton,
9461
9689
  {
9462
- ...props,
9690
+ ...baseButtonProps,
9463
9691
  "aria-selected": isFocused ? "true" : "false",
9464
9692
  onBlur: (e) => {
9465
9693
  setIsFocused(false);
@@ -9508,12 +9736,14 @@ function ContextMenuContent({
9508
9736
  backLabel = "Back",
9509
9737
  children,
9510
9738
  className,
9739
+ enableAnimations = true,
9511
9740
  Header: Header2,
9512
9741
  items,
9513
9742
  ItemsWrapper,
9514
9743
  menuClassName,
9515
9744
  onClose,
9516
9745
  onMenuLevelChange,
9746
+ transitionDirection,
9517
9747
  ...props
9518
9748
  }) {
9519
9749
  const rootLevel = React.useMemo(
@@ -9526,6 +9756,7 @@ function ContextMenuContent({
9526
9756
  [Header2, items, ItemsWrapper, menuClassName]
9527
9757
  );
9528
9758
  const [menuStack, setMenuStack] = React.useState(() => [rootLevel]);
9759
+ const [menuBodyAnimationKey, setMenuBodyAnimationKey] = React.useState(0);
9529
9760
  const activeMenu = menuStack[menuStack.length - 1];
9530
9761
  const ActiveMenuItemsWrapper = activeMenu.ItemsWrapper ?? React.Fragment;
9531
9762
  const closeMenu = React.useCallback(() => {
@@ -9549,10 +9780,9 @@ function ContextMenuContent({
9549
9780
  [ItemsWrapper]
9550
9781
  );
9551
9782
  const returnToParentMenu = React.useCallback(() => {
9552
- setMenuStack(
9553
- (current) => current.length > 1 ? current.slice(0, current.length - 1) : current
9554
- );
9555
- }, []);
9783
+ if (menuStack.length <= 1) return;
9784
+ setMenuStack((current) => current.slice(0, current.length - 1));
9785
+ }, [menuStack.length]);
9556
9786
  React.useEffect(() => {
9557
9787
  setMenuStack((current) => {
9558
9788
  if (current.length === 1 && current[0] === rootLevel) return current;
@@ -9562,42 +9792,116 @@ function ContextMenuContent({
9562
9792
  React.useEffect(() => {
9563
9793
  onMenuLevelChange?.(menuStack.length);
9564
9794
  }, [menuStack.length, onMenuLevelChange]);
9565
- return /* @__PURE__ */ jsxRuntime.jsx(ContextMenuContext.Provider, { value: { closeMenu, openSubmenu, returnToParentMenu }, children: /* @__PURE__ */ jsxRuntime.jsxs(ContextMenuRoot, { className: clsx(className, activeMenu.menuClassName), ...props, children: [
9566
- activeMenu.Header ? /* @__PURE__ */ jsxRuntime.jsx(activeMenu.Header, {}) : menuStack.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(ContextMenuHeader, { children: /* @__PURE__ */ jsxRuntime.jsxs(ContextMenuBackButton, { onClick: returnToParentMenu, children: [
9567
- /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconChevronLeft, {}),
9568
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: backLabel })
9569
- ] }) }) : null,
9570
- /* @__PURE__ */ jsxRuntime.jsx(ContextMenuBody, { children: activeMenu.Submenu ? /* @__PURE__ */ jsxRuntime.jsx(activeMenu.Submenu, {}) : /* @__PURE__ */ jsxRuntime.jsx(ActiveMenuItemsWrapper, { children: typeof children !== "undefined" ? children : activeMenu.items?.map((Item2, index) => /* @__PURE__ */ jsxRuntime.jsx(Item2, {}, `context-menu-item-${index}`)) }) })
9571
- ] }) });
9795
+ React.useEffect(() => {
9796
+ if (!transitionDirection) return;
9797
+ setMenuBodyAnimationKey((value) => value + 1);
9798
+ }, [transitionDirection, menuStack.length]);
9799
+ return /* @__PURE__ */ jsxRuntime.jsx(ContextMenuContext.Provider, { value: { closeMenu, openSubmenu, returnToParentMenu }, children: /* @__PURE__ */ jsxRuntime.jsxs(
9800
+ ContextMenuRoot,
9801
+ {
9802
+ className: clsx(className, activeMenu.menuClassName),
9803
+ "data-str-chat-enable-animations": enableAnimations,
9804
+ ...props,
9805
+ children: [
9806
+ activeMenu.Header ? /* @__PURE__ */ jsxRuntime.jsx(activeMenu.Header, {}) : menuStack.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(ContextMenuHeader, { children: /* @__PURE__ */ jsxRuntime.jsxs(ContextMenuBackButton, { onClick: returnToParentMenu, children: [
9807
+ /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconChevronLeft, {}),
9808
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: backLabel })
9809
+ ] }) }) : null,
9810
+ /* @__PURE__ */ jsxRuntime.jsx(
9811
+ ContextMenuBody,
9812
+ {
9813
+ className: clsx({
9814
+ "str-chat__context-menu__body--submenu-backward": transitionDirection === "backward",
9815
+ "str-chat__context-menu__body--submenu-forward": transitionDirection === "forward"
9816
+ }),
9817
+ children: activeMenu.Submenu ? /* @__PURE__ */ jsxRuntime.jsx(activeMenu.Submenu, {}) : /* @__PURE__ */ jsxRuntime.jsx(ActiveMenuItemsWrapper, { children: typeof children !== "undefined" ? children : activeMenu.items?.map((Item2, index) => /* @__PURE__ */ jsxRuntime.jsx(Item2, {}, `context-menu-item-${index}`)) })
9818
+ },
9819
+ `context-menu-body-${menuStack.length}-${menuBodyAnimationKey}`
9820
+ )
9821
+ ]
9822
+ }
9823
+ ) });
9572
9824
  }
9573
9825
  const ContextMenu = (props) => {
9826
+ const { ContextMenuContent: ContextMenuContentComponent = ContextMenuContent } = WithAudioPlayback.useComponentContext();
9574
9827
  const {
9575
9828
  allowFlip,
9829
+ closeOnClickOutside,
9830
+ closeTransitionMs = 130,
9576
9831
  dialogManagerId,
9577
9832
  focus: focus2,
9578
9833
  id,
9579
9834
  placement,
9580
9835
  referenceElement,
9836
+ submenuTransitionDurationMs,
9581
9837
  tabIndex,
9582
9838
  trapFocus,
9583
9839
  ...menuProps
9584
9840
  } = props;
9841
+ const resolvedSubmenuTransitionDurationMs = submenuTransitionDurationMs ?? 460;
9585
9842
  const isAnchored = id != null;
9586
9843
  const [menuLevel, setMenuLevel] = React.useState(1);
9844
+ const [transitionDirection, setTransitionDirection] = React.useState(void 0);
9845
+ const [contentResetToken, setContentResetToken] = React.useState(0);
9846
+ const transitionTimeoutRef = React.useRef(null);
9847
+ const previousMenuLevelRef = React.useRef(1);
9587
9848
  const open = useDialogIsOpen(id ?? "", dialogManagerId);
9849
+ const previousOpenRef = React.useRef(open);
9588
9850
  React.useEffect(() => {
9589
- if (isAnchored && !open) setMenuLevel(1);
9851
+ if (!isAnchored) return;
9852
+ if (previousOpenRef.current && !open) {
9853
+ setMenuLevel(1);
9854
+ setTransitionDirection(void 0);
9855
+ setContentResetToken((value) => value + 1);
9856
+ previousMenuLevelRef.current = 1;
9857
+ if (transitionTimeoutRef.current) {
9858
+ clearTimeout(transitionTimeoutRef.current);
9859
+ transitionTimeoutRef.current = null;
9860
+ }
9861
+ }
9862
+ previousOpenRef.current = open;
9590
9863
  }, [isAnchored, open]);
9591
- const content = /* @__PURE__ */ jsxRuntime.jsx(
9592
- ContextMenuContent,
9864
+ React.useEffect(
9865
+ () => () => {
9866
+ if (transitionTimeoutRef.current) {
9867
+ clearTimeout(transitionTimeoutRef.current);
9868
+ }
9869
+ },
9870
+ []
9871
+ );
9872
+ const handleMenuLevelChange = React.useCallback(
9873
+ (level) => {
9874
+ if (isAnchored) {
9875
+ const previousLevel = previousMenuLevelRef.current;
9876
+ if (level !== previousLevel) {
9877
+ setTransitionDirection(level > previousLevel ? "forward" : "backward");
9878
+ if (transitionTimeoutRef.current) clearTimeout(transitionTimeoutRef.current);
9879
+ transitionTimeoutRef.current = setTimeout(() => {
9880
+ setTransitionDirection(void 0);
9881
+ transitionTimeoutRef.current = null;
9882
+ }, resolvedSubmenuTransitionDurationMs);
9883
+ }
9884
+ previousMenuLevelRef.current = level;
9885
+ setMenuLevel(level);
9886
+ return;
9887
+ }
9888
+ menuProps.onMenuLevelChange?.(level);
9889
+ },
9890
+ [isAnchored, menuProps, resolvedSubmenuTransitionDurationMs]
9891
+ );
9892
+ const content = /* @__PURE__ */ React.createElement(
9893
+ ContextMenuContentComponent,
9593
9894
  {
9594
9895
  ...menuProps,
9595
- onMenuLevelChange: isAnchored ? setMenuLevel : menuProps.onMenuLevelChange
9896
+ key: `context-menu-content-${contentResetToken}`,
9897
+ onMenuLevelChange: handleMenuLevelChange,
9898
+ transitionDirection
9596
9899
  }
9597
9900
  );
9598
9901
  if (isAnchored) {
9599
9902
  const {
9600
9903
  backLabel: _b,
9904
+ enableAnimations: _ea,
9601
9905
  Header: _h,
9602
9906
  items: _i,
9603
9907
  ItemsWrapper: _w,
@@ -9610,6 +9914,8 @@ const ContextMenu = (props) => {
9610
9914
  DialogAnchor,
9611
9915
  {
9612
9916
  allowFlip,
9917
+ closeOnClickOutside,
9918
+ closeTransitionMs,
9613
9919
  dialogManagerId,
9614
9920
  focus: focus2,
9615
9921
  id,
@@ -9947,7 +10253,9 @@ const useBaseMessageActionSetFilter = (messageActionSet, disable = false) => {
9947
10253
  if (isBounced || isInitialMessage || // not sure whether this thing even works anymore
9948
10254
  !message.type || message.type === "system" || message.type === "ephemeral" || message.status === "sending")
9949
10255
  return [];
9950
- return messageActionSet.filter(({ type }) => {
10256
+ return messageActionSet.filter((action) => {
10257
+ if (action.placement === "quick-dropdown-toggle") return true;
10258
+ const type = action.type;
9951
10259
  if (WithAudioPlayback.ACTIONS_NOT_WORKING_IN_THREAD.includes(type) && isMessageThreadReply)
9952
10260
  return false;
9953
10261
  if (message.error) {
@@ -11855,6 +12163,7 @@ const toBaseImageDescriptors = (attachment, options = {}) => {
11855
12163
  title: attachment.title
11856
12164
  };
11857
12165
  }
12166
+ return void 0;
11858
12167
  };
11859
12168
  const BASE_FILE_ICON_CLASSNAME = "str-chat__file-icon";
11860
12169
  const FILE_ICON_GRAPHIC_CLASSNAME = "str-chat__file-icon__graphic";
@@ -14692,7 +15001,8 @@ const PollOptionWithVotes = ({
14692
15001
  "div",
14693
15002
  {
14694
15003
  className: clsx("str-chat__poll-option", {
14695
- "str-chat__poll-option--has-more-votes": isVotesPreview && voteCount > countVotesPreview
15004
+ "str-chat__poll-option--has-more-votes": isVotesPreview && voteCount > countVotesPreview,
15005
+ "str-chat__poll-option--has-votes": voteCount
14696
15006
  }),
14697
15007
  children: [
14698
15008
  /* @__PURE__ */ jsxRuntime.jsx(PollOptionWithVotesHeader, { option, optionOrderNumber: orderNumber }),
@@ -15705,7 +16015,7 @@ const AttachmentSelector = ({
15705
16015
  getModalPortalDestination
15706
16016
  }) => {
15707
16017
  const { t } = WithAudioPlayback.useTranslationContext();
15708
- const { Modal = GlobalModal } = WithAudioPlayback.useComponentContext();
16018
+ const { ContextMenu: ContextMenuComponent = ContextMenu, Modal = GlobalModal } = WithAudioPlayback.useComponentContext();
15709
16019
  const { channelCapabilities } = WithAudioPlayback.useChannelStateContext();
15710
16020
  const messageComposer = WithAudioPlayback.useMessageComposerController();
15711
16021
  const isCooldownActive = WithAudioPlayback.useIsCooldownActive();
@@ -15765,7 +16075,7 @@ const AttachmentSelector = ({
15765
16075
  }
15766
16076
  ),
15767
16077
  /* @__PURE__ */ jsxRuntime.jsx(
15768
- ContextMenu,
16078
+ ContextMenuComponent,
15769
16079
  {
15770
16080
  allowFlip: true,
15771
16081
  backLabel: t("Back"),
@@ -17795,7 +18105,10 @@ const SuggestionList = ({
17795
18105
  setFocusedItemIndex,
17796
18106
  suggestionItemComponents = defaultComponents
17797
18107
  }) => {
17798
- const { AutocompleteSuggestionItem = SuggestionListItem } = WithAudioPlayback.useComponentContext();
18108
+ const {
18109
+ AutocompleteSuggestionItem = SuggestionListItem,
18110
+ ContextMenu: ContextMenuComponent = ContextMenu
18111
+ } = WithAudioPlayback.useComponentContext();
17799
18112
  const { textareaRef } = WithAudioPlayback.useMessageComposerContext();
17800
18113
  const messageComposer = WithAudioPlayback.useMessageComposerController();
17801
18114
  const { textComposer } = messageComposer;
@@ -17913,7 +18226,7 @@ const SuggestionList = ({
17913
18226
  zIndex: 1e3
17914
18227
  },
17915
18228
  children: /* @__PURE__ */ jsxRuntime.jsx(
17916
- ContextMenu,
18229
+ ContextMenuComponent,
17917
18230
  {
17918
18231
  className: clsx("str-chat__suggestion-list", className),
17919
18232
  Header: suggestions.searchSource.type === "commands" ? CommandsMenuHeader : void 0,
@@ -18139,12 +18452,6 @@ const TextareaComposer = ({
18139
18452
  if (!textareaRef.current || textareaIsFocused || !focus2) return;
18140
18453
  textareaRef.current.focus();
18141
18454
  }, [attachments, focus2, quotedMessage, textareaRef]);
18142
- React.useEffect(
18143
- () => () => {
18144
- messageComposer.clear();
18145
- },
18146
- [messageComposer]
18147
- );
18148
18455
  React.useLayoutEffect(() => {
18149
18456
  const textarea = textareaRef.current;
18150
18457
  if (!textarea || isComposing) return;
@@ -18598,7 +18905,7 @@ const MessageComposerProvider = (props) => {
18598
18905
  const messageComposer = WithAudioPlayback.useMessageComposerController();
18599
18906
  React.useEffect(
18600
18907
  () => () => {
18601
- messageComposer.createDraft();
18908
+ messageComposer.createDraft().finally(() => messageComposer.clear());
18602
18909
  },
18603
18910
  [messageComposer]
18604
18911
  );
@@ -18916,7 +19223,7 @@ const useChat = ({
18916
19223
  };
18917
19224
  React.useEffect(() => {
18918
19225
  if (!client) return;
18919
- const version = "14.0.0-beta.2";
19226
+ const version = "14.0.0-beta.3";
18920
19227
  const userAgent = client.getUserAgent();
18921
19228
  if (!userAgent.includes("stream-chat-react")) {
18922
19229
  client.setUserAgent(`stream-chat-react-${version}-${userAgent}`);
@@ -19417,10 +19724,10 @@ const ReactionSelector = (props) => {
19417
19724
  ) })
19418
19725
  ] });
19419
19726
  };
19420
- ReactionSelector.getDialogId = (({ messageId, threadList }) => {
19727
+ ReactionSelector.getDialogId = ({ messageId, threadList }) => {
19421
19728
  const dialogIdNamespace = threadList ? "-thread" : "";
19422
- return `reaction-selector${dialogIdNamespace}--${messageId}`;
19423
- });
19729
+ return `reaction-selector${dialogIdNamespace}-${messageId}`;
19730
+ };
19424
19731
  ReactionSelector.displayName = "ReactionSelector";
19425
19732
  const ReactionSelectorWithButton = ({
19426
19733
  ReactionIcon
@@ -19505,6 +19812,7 @@ const QuickMessageActionsButton = ({ className, ...props }) => /* @__PURE__ */ j
19505
19812
  appearance: "ghost",
19506
19813
  circular: true,
19507
19814
  className: clsx("str-chat__message-actions-box-button", className),
19815
+ size: "sm",
19508
19816
  variant: "secondary",
19509
19817
  ...props
19510
19818
  }
@@ -19865,6 +20173,32 @@ const DefaultMessageActionComponents = {
19865
20173
  }
19866
20174
  },
19867
20175
  quick: {
20176
+ // eslint-disable-next-line react/display-name
20177
+ DropdownToggle: React.forwardRef((_, ref) => {
20178
+ const { t } = WithAudioPlayback.useTranslationContext();
20179
+ const { message } = useMessageContext();
20180
+ const dropdownDialogIsOpen = useDialogIsOpen(
20181
+ MessageActions.getDialogId({ messageId: message.id })
20182
+ );
20183
+ const { dialog } = useDialogOnNearestManager({
20184
+ id: MessageActions.getDialogId({ messageId: message.id })
20185
+ });
20186
+ return /* @__PURE__ */ jsxRuntime.jsx(
20187
+ QuickMessageActionsButton,
20188
+ {
20189
+ "aria-expanded": dropdownDialogIsOpen,
20190
+ "aria-haspopup": "true",
20191
+ "aria-label": t("aria/Open Message Actions Menu"),
20192
+ className: "str-chat__message-actions-box-button",
20193
+ "data-testid": "message-actions-toggle-button",
20194
+ onClick: () => {
20195
+ dialog?.toggle();
20196
+ },
20197
+ ref,
20198
+ children: /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconDotGrid1x3Horizontal, { className: "str-chat__message-action-icon" })
20199
+ }
20200
+ );
20201
+ }),
19868
20202
  React() {
19869
20203
  return /* @__PURE__ */ jsxRuntime.jsx(ReactionSelectorWithButton, { ReactionIcon: WithAudioPlayback.IconEmojiSmile });
19870
20204
  },
@@ -19885,7 +20219,10 @@ const DefaultMessageActionComponents = {
19885
20219
  }
19886
20220
  };
19887
20221
  const defaultMessageActionSet = [
19888
- // { placement: 'dropdown', type: 'block' },
20222
+ {
20223
+ Component: DefaultMessageActionComponents.quick.DropdownToggle,
20224
+ placement: "quick-dropdown-toggle"
20225
+ },
19889
20226
  {
19890
20227
  Component: DefaultMessageActionComponents.quick.Reply,
19891
20228
  placement: "quick",
@@ -19975,14 +20312,14 @@ function useFetchReactions(options) {
19975
20312
  const handleFetchReactions = propHandleFetchReactions ?? contextHandleFetchReactions;
19976
20313
  const [refetchNonce, setRefetchNonce] = React.useState(null);
19977
20314
  React.useEffect(() => {
19978
- if (!shouldFetch || !reactionType) {
20315
+ if (!shouldFetch) {
19979
20316
  return;
19980
20317
  }
19981
20318
  let cancel = false;
19982
20319
  (async () => {
19983
20320
  try {
19984
20321
  setIsLoading(true);
19985
- const reactions2 = await handleFetchReactions(reactionType, sort);
20322
+ const reactions2 = await handleFetchReactions(reactionType ?? void 0, sort);
19986
20323
  if (!cancel) {
19987
20324
  setReactions(reactions2);
19988
20325
  }
@@ -20006,6 +20343,16 @@ function useFetchReactions(options) {
20006
20343
  return { isLoading, reactions, refetch };
20007
20344
  }
20008
20345
  const defaultReactionDetailsSort = { created_at: -1 };
20346
+ const MessageReactionsDetailLoadingIndicator = () => {
20347
+ const elements = React.useMemo(
20348
+ () => Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "str-chat__message-reactions-detail__skeleton-item", children: [
20349
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-avatar" }),
20350
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-line" })
20351
+ ] }, index)),
20352
+ []
20353
+ );
20354
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: elements });
20355
+ };
20009
20356
  function MessageReactionsDetail({
20010
20357
  handleFetchReactions,
20011
20358
  onSelectedReactionTypeChange,
@@ -20016,7 +20363,11 @@ function MessageReactionsDetail({
20016
20363
  totalReactionCount
20017
20364
  }) {
20018
20365
  const { client } = WithAudioPlayback.useChatContext();
20019
- const { Avatar: Avatar$1 = Avatar } = WithAudioPlayback.useComponentContext(MessageReactionsDetail.name);
20366
+ const {
20367
+ Avatar: Avatar$1 = Avatar,
20368
+ LoadingIndicator: LoadingIndicator2 = MessageReactionsDetailLoadingIndicator,
20369
+ reactionOptions = defaultReactionOptions
20370
+ } = WithAudioPlayback.useComponentContext(MessageReactionsDetail.name);
20020
20371
  const { t } = WithAudioPlayback.useTranslationContext();
20021
20372
  const {
20022
20373
  handleReaction: contextHandleReaction,
@@ -20043,7 +20394,7 @@ function MessageReactionsDetail({
20043
20394
  "div",
20044
20395
  {
20045
20396
  className: "str-chat__message-reactions-detail",
20046
- "data-testid": "reactions-list-modal",
20397
+ "data-testid": "message-reactions-detail",
20047
20398
  children: [
20048
20399
  typeof totalReactionCount === "number" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__total-count", children: t("{{ count }} reactions", { count: totalReactionCount }) }),
20049
20400
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__reaction-type-list-container", children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "str-chat__message-reactions-detail__reaction-type-list", children: reactions.map(
@@ -20056,7 +20407,9 @@ function MessageReactionsDetail({
20056
20407
  {
20057
20408
  "aria-pressed": reactionType === selectedReactionType,
20058
20409
  className: "str-chat__message-reactions-detail__reaction-type-list-item-button",
20059
- onClick: () => onSelectedReactionTypeChange?.(reactionType),
20410
+ onClick: () => onSelectedReactionTypeChange?.(
20411
+ selectedReactionType === reactionType ? null : reactionType
20412
+ ),
20060
20413
  children: [
20061
20414
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "str-chat__message-reactions-detail__reaction-type-list-item-icon", children: /* @__PURE__ */ jsxRuntime.jsx(EmojiComponent, {}) }),
20062
20415
  reactionCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -20080,22 +20433,10 @@ function MessageReactionsDetail({
20080
20433
  className: "str-chat__message-reactions-detail__user-list",
20081
20434
  "data-testid": "all-reacting-users",
20082
20435
  children: [
20083
- areReactionsLoading && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
20084
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "str-chat__message-reactions-detail__skeleton-item", children: [
20085
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-avatar" }),
20086
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-line" })
20087
- ] }),
20088
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "str-chat__message-reactions-detail__skeleton-item", children: [
20089
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-avatar" }),
20090
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-line" })
20091
- ] }),
20092
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "str-chat__message-reactions-detail__skeleton-item", children: [
20093
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-avatar" }),
20094
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-reactions-detail__skeleton-line" })
20095
- ] })
20096
- ] }),
20097
- !areReactionsLoading && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reactionDetailsWithLegacyFallback.map(({ user }) => {
20436
+ areReactionsLoading && /* @__PURE__ */ jsxRuntime.jsx(LoadingIndicator2, {}),
20437
+ !areReactionsLoading && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reactionDetailsWithLegacyFallback.map(({ type, user }) => {
20098
20438
  const belongsToCurrentUser = client.user?.id === user?.id;
20439
+ const EmojiComponent = Array.isArray(reactionOptions) ? void 0 : reactionOptions.quick[type]?.Component ?? reactionOptions.extended?.[type]?.Component;
20099
20440
  return /* @__PURE__ */ jsxRuntime.jsxs(
20100
20441
  "div",
20101
20442
  {
@@ -20120,23 +20461,23 @@ function MessageReactionsDetail({
20120
20461
  children: belongsToCurrentUser ? t("You") : user?.name || user?.id
20121
20462
  }
20122
20463
  ),
20123
- belongsToCurrentUser && selectedReactionType && /* @__PURE__ */ jsxRuntime.jsx(
20464
+ belongsToCurrentUser && /* @__PURE__ */ jsxRuntime.jsx(
20124
20465
  "button",
20125
20466
  {
20126
20467
  className: "str-chat__message-reactions-detail__user-list-item-button",
20127
20468
  "data-testid": "remove-reaction-button",
20128
- onClick: (e) => {
20129
- contextHandleReaction(selectedReactionType, e).then(() => {
20130
- refetch();
20131
- });
20469
+ onClick: async (e) => {
20470
+ await contextHandleReaction(type, e);
20471
+ refetch();
20132
20472
  },
20133
20473
  children: t("Tap to remove")
20134
20474
  }
20135
20475
  )
20136
- ] })
20476
+ ] }),
20477
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "str-chat__message-reactions-detail__user-list-item-icon", children: EmojiComponent && !selectedReactionType && /* @__PURE__ */ jsxRuntime.jsx(EmojiComponent, {}) })
20137
20478
  ]
20138
20479
  },
20139
- user?.id
20480
+ `${user?.id}-${type}`
20140
20481
  );
20141
20482
  }) })
20142
20483
  ]
@@ -20285,6 +20626,7 @@ const UnMemoizedMessageReactions = (props) => {
20285
20626
  const divRef = React.useRef(null);
20286
20627
  const dialogId2 = `message-reactions-detail-${message.id}`;
20287
20628
  const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId2 });
20629
+ const isDialogOpen = useDialogIsOpen(dialogId2, dialogManager?.id);
20288
20630
  const handleReactionButtonClick = (reactionType) => {
20289
20631
  if (totalReactionCount > MAX_MESSAGE_REACTIONS_TO_FETCH) {
20290
20632
  return;
@@ -20319,6 +20661,8 @@ const UnMemoizedMessageReactions = (props) => {
20319
20661
  children: /* @__PURE__ */ jsxRuntime.jsxs(
20320
20662
  FragmentOrButton,
20321
20663
  {
20664
+ "aria-expanded": isDialogOpen,
20665
+ "aria-pressed": isDialogOpen,
20322
20666
  buttonIf: visualStyle === "clustered",
20323
20667
  className: "str-chat__message-reactions__list-button",
20324
20668
  onClick: () => handleReactionButtonClick(existingReactions[0]?.reactionType ?? null),
@@ -20356,9 +20700,7 @@ const UnMemoizedMessageReactions = (props) => {
20356
20700
  "button",
20357
20701
  {
20358
20702
  className: "str-chat__message-reactions__list-item-button",
20359
- onClick: () => handleReactionButtonClick(
20360
- existingReactions.at(-1)?.reactionType ?? null
20361
- ),
20703
+ onClick: () => handleReactionButtonClick(null),
20362
20704
  children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "str-chat__message-reactions__overflow-count", children: [
20363
20705
  "+",
20364
20706
  totalReactionCount - cappedExistingReactions.reactionCountToDisplay
@@ -20483,91 +20825,74 @@ const StreamEmoji = ({
20483
20825
  const useSplitActionSet = (actionSet) => React.useMemo(() => {
20484
20826
  const quickActionSet = [];
20485
20827
  const dropdownActionSet = [];
20828
+ let quickDropdownToggleAction;
20486
20829
  for (const action of actionSet) {
20487
20830
  if (action.placement === "quick")
20488
20831
  quickActionSet.push(action);
20489
20832
  if (action.placement === "dropdown")
20490
20833
  dropdownActionSet.push(action);
20834
+ if (action.placement === "quick-dropdown-toggle") {
20835
+ quickDropdownToggleAction ?? (quickDropdownToggleAction = action);
20836
+ }
20491
20837
  }
20492
- return { dropdownActionSet, quickActionSet };
20838
+ return { dropdownActionSet, quickActionSet, quickDropdownToggleAction };
20493
20839
  }, [actionSet]);
20494
20840
  const MessageActions = ({
20495
20841
  disableBaseMessageActionSetFilter = false,
20496
20842
  messageActionSet = defaultMessageActionSet
20497
20843
  }) => {
20498
- const { theme } = WithAudioPlayback.useChatContext();
20499
20844
  const { isMyMessage, message, threadList } = useMessageContext();
20845
+ const { ContextMenu: ContextMenuComponent = ContextMenu } = WithAudioPlayback.useComponentContext();
20500
20846
  const { t } = WithAudioPlayback.useTranslationContext();
20501
20847
  const [actionsBoxButtonElement, setActionsBoxButtonElement] = React.useState(null);
20502
20848
  const filteredMessageActionSet = useBaseMessageActionSetFilter(
20503
20849
  messageActionSet,
20504
20850
  disableBaseMessageActionSetFilter
20505
20851
  );
20506
- const { dropdownActionSet, quickActionSet } = useSplitActionSet(
20507
- filteredMessageActionSet
20508
- );
20509
- const dropdownDialogId = `message-actions--${message.id}`;
20852
+ const { dropdownActionSet, quickActionSet, quickDropdownToggleAction } = useSplitActionSet(filteredMessageActionSet);
20853
+ const messageActionsDialogId = MessageActions.getDialogId({ messageId: message.id });
20510
20854
  const reactionSelectorDialogId = ReactionSelector.getDialogId({
20511
20855
  messageId: message.id,
20512
20856
  threadList
20513
20857
  });
20514
- const { dialog, dialogManager } = useDialogOnNearestManager({ id: dropdownDialogId });
20515
- const dropdownDialogIsOpen = useDialogIsOpen(dropdownDialogId, dialogManager?.id);
20858
+ const { dialog, dialogManager } = useDialogOnNearestManager({
20859
+ id: messageActionsDialogId
20860
+ });
20861
+ const messageActionsDialogIsOpen = useDialogIsOpen(
20862
+ messageActionsDialogId,
20863
+ dialogManager?.id
20864
+ );
20516
20865
  const reactionSelectorDialogIsOpen = useDialogIsOpen(
20517
20866
  reactionSelectorDialogId,
20518
20867
  dialogManager?.id
20519
20868
  );
20520
- const contextMenuItems = React.useMemo(
20521
- () => dropdownActionSet.map(({ Component }) => {
20522
- const ActionItem = (menuProps) => /* @__PURE__ */ jsxRuntime.jsx(Component, { ...menuProps });
20523
- return ActionItem;
20524
- }),
20525
- [dropdownActionSet]
20526
- );
20527
20869
  if (dropdownActionSet.length + quickActionSet.length === 0) {
20528
20870
  return null;
20529
20871
  }
20530
20872
  return /* @__PURE__ */ jsxRuntime.jsxs(
20531
20873
  "div",
20532
20874
  {
20533
- className: clsx(`str-chat__message-${theme}__actions str-chat__message-options`, {
20534
- "str-chat__message-options--active": dropdownDialogIsOpen || reactionSelectorDialogIsOpen
20875
+ className: clsx("str-chat__message-options", {
20876
+ "str-chat__message-options--active": messageActionsDialogIsOpen || reactionSelectorDialogIsOpen
20535
20877
  }),
20536
20878
  children: [
20537
- dropdownActionSet.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
20538
- /* @__PURE__ */ jsxRuntime.jsx(
20539
- WithAudioPlayback.Button,
20540
- {
20541
- appearance: "ghost",
20542
- "aria-expanded": dropdownDialogIsOpen,
20543
- "aria-haspopup": "true",
20544
- "aria-label": t("aria/Open Message Actions Menu"),
20545
- circular: true,
20546
- className: "str-chat__message-actions-box-button",
20547
- "data-testid": "message-actions-toggle-button",
20548
- onClick: () => {
20549
- dialog?.toggle();
20550
- },
20551
- ref: setActionsBoxButtonElement,
20552
- variant: "secondary",
20553
- children: /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconDotGrid1x3Horizontal, { className: "str-chat__message-action-icon" })
20554
- }
20555
- ),
20879
+ quickDropdownToggleAction && dropdownActionSet.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
20880
+ /* @__PURE__ */ jsxRuntime.jsx(quickDropdownToggleAction.Component, { ref: setActionsBoxButtonElement }),
20556
20881
  /* @__PURE__ */ jsxRuntime.jsx(
20557
- ContextMenu,
20882
+ ContextMenuComponent,
20558
20883
  {
20559
20884
  backLabel: t("Back"),
20560
20885
  className: clsx("str-chat__message-actions-box", {
20561
- "str-chat__message-actions-box--open": dropdownDialogIsOpen
20886
+ "str-chat__message-actions-box--open": messageActionsDialogIsOpen
20562
20887
  }),
20563
20888
  dialogManagerId: dialogManager?.id,
20564
- id: dropdownDialogId,
20565
- items: contextMenuItems,
20889
+ id: messageActionsDialogId,
20566
20890
  onClose: dialog?.close,
20567
20891
  placement: isMyMessage() ? "top-end" : "top-start",
20568
20892
  referenceElement: actionsBoxButtonElement,
20569
20893
  tabIndex: -1,
20570
- trapFocus: true
20894
+ trapFocus: true,
20895
+ children: dropdownActionSet.map(({ Component, type }) => /* @__PURE__ */ jsxRuntime.jsx(Component, {}, type))
20571
20896
  }
20572
20897
  )
20573
20898
  ] }),
@@ -20576,6 +20901,8 @@ const MessageActions = ({
20576
20901
  }
20577
20902
  );
20578
20903
  };
20904
+ MessageActions.getDialogId = ({ messageId }) => `message-actions-${messageId}`;
20905
+ MessageActions.displayName = "MessageActions";
20579
20906
  const MessageUIWithContext = ({
20580
20907
  endOfGroup,
20581
20908
  firstOfGroup,
@@ -21402,6 +21729,7 @@ const useScrollLocationLogic = (params) => {
21402
21729
  const closeToBottom = React.useRef(false);
21403
21730
  const closeToTop = React.useRef(false);
21404
21731
  const previousScrollTopRef = React.useRef(0);
21732
+ const previousMessagesLengthRef = React.useRef(messages.length);
21405
21733
  const anchorRestoreCleanupRef = React.useRef(null);
21406
21734
  const captureAnchor = React.useCallback(() => {
21407
21735
  if (!listElement) return null;
@@ -21553,6 +21881,60 @@ const useScrollLocationLogic = (params) => {
21553
21881
  scrollToBottom();
21554
21882
  }
21555
21883
  }, [disableAutoScrollToBottom, justReachedLatestMessageSet, listElement, hasMoreNewer]);
21884
+ React.useLayoutEffect(() => {
21885
+ if (!listElement || disableAutoScrollToBottom || hasMoreNewer || suppressAutoscroll || justReachedLatestMessageSet || isRestoringOlderAnchorRef.current) {
21886
+ return;
21887
+ }
21888
+ const initialDistanceToBottom = listElement.scrollHeight - (listElement.scrollTop + listElement.clientHeight);
21889
+ const messagesHydrated = previousMessagesLengthRef.current === 0 && messages.length > 0;
21890
+ if (initialDistanceToBottom > scrolledUpThreshold && !messagesHydrated) {
21891
+ return;
21892
+ }
21893
+ let keepPinnedToBottom = true;
21894
+ const maybeScrollToBottom = () => {
21895
+ if (keepPinnedToBottom) {
21896
+ scrollToBottom();
21897
+ }
21898
+ };
21899
+ maybeScrollToBottom();
21900
+ const settleDelays = [80, messagesHydrated ? 260 : 420, 900, 1700];
21901
+ const settleTimeoutIds = settleDelays.map(
21902
+ (delay) => setTimeout(maybeScrollToBottom, delay)
21903
+ );
21904
+ const stopKeepingPinnedToBottom = () => {
21905
+ keepPinnedToBottom = false;
21906
+ };
21907
+ listElement.addEventListener("pointerdown", stopKeepingPinnedToBottom, {
21908
+ passive: true
21909
+ });
21910
+ listElement.addEventListener("touchstart", stopKeepingPinnedToBottom, {
21911
+ passive: true
21912
+ });
21913
+ listElement.addEventListener("wheel", stopKeepingPinnedToBottom, {
21914
+ passive: true
21915
+ });
21916
+ listElement.addEventListener("keydown", stopKeepingPinnedToBottom);
21917
+ const pinWindowTimeoutId = setTimeout(() => {
21918
+ stopKeepingPinnedToBottom();
21919
+ }, 2200);
21920
+ return () => {
21921
+ settleTimeoutIds.forEach(clearTimeout);
21922
+ clearTimeout(pinWindowTimeoutId);
21923
+ listElement.removeEventListener("pointerdown", stopKeepingPinnedToBottom);
21924
+ listElement.removeEventListener("touchstart", stopKeepingPinnedToBottom);
21925
+ listElement.removeEventListener("wheel", stopKeepingPinnedToBottom);
21926
+ listElement.removeEventListener("keydown", stopKeepingPinnedToBottom);
21927
+ };
21928
+ }, [
21929
+ disableAutoScrollToBottom,
21930
+ hasMoreNewer,
21931
+ justReachedLatestMessageSet,
21932
+ listElement,
21933
+ messages.length,
21934
+ scrollToBottom,
21935
+ scrolledUpThreshold,
21936
+ suppressAutoscroll
21937
+ ]);
21556
21938
  const updateScrollTop = useMessageListScrollManager({
21557
21939
  captureAnchor,
21558
21940
  disableScrollManagement: disableScrollManagement || isRestoringOlderAnchorRef.current,
@@ -21579,6 +21961,9 @@ const useScrollLocationLogic = (params) => {
21579
21961
  React.useLayoutEffect(() => {
21580
21962
  previousHasMoreNewerRef.current = hasMoreNewer;
21581
21963
  }, [hasMoreNewer]);
21964
+ React.useLayoutEffect(() => {
21965
+ previousMessagesLengthRef.current = messages.length;
21966
+ }, [messages.length]);
21582
21967
  const onScroll = React.useCallback(
21583
21968
  (event) => {
21584
21969
  const element = event.target;
@@ -21587,8 +21972,10 @@ const useScrollLocationLogic = (params) => {
21587
21972
  updateScrollTop(scrollTop, captureAnchor);
21588
21973
  const offsetHeight = element.offsetHeight;
21589
21974
  const scrollHeight = element.scrollHeight;
21975
+ const distanceToBottom = scrollHeight - (scrollTop + offsetHeight);
21976
+ const bottomEnterThreshold = Math.max(Math.floor(scrolledUpThreshold * 0.6), 24);
21590
21977
  const prevCloseToBottom = closeToBottom.current;
21591
- closeToBottom.current = scrollHeight - (scrollTop + offsetHeight) < scrolledUpThreshold;
21978
+ closeToBottom.current = prevCloseToBottom ? distanceToBottom < scrolledUpThreshold : distanceToBottom < bottomEnterThreshold;
21592
21979
  closeToTop.current = scrollTop < scrolledUpThreshold;
21593
21980
  if (closeToBottom.current) {
21594
21981
  setHasNewMessages(false);
@@ -23523,11 +23910,17 @@ const useConnectionRecoveredListener = (forceUpdate) => {
23523
23910
  const MOBILE_NAV_BREAKPOINT = 768;
23524
23911
  const useMobileNavigation = (channelListRef, navOpen, closeMobileNav) => {
23525
23912
  React.useEffect(() => {
23913
+ const isClickInsideChannelList = (event) => {
23914
+ const channelListElement = channelListRef.current;
23915
+ if (!channelListElement) return false;
23916
+ const eventPath = event.composedPath();
23917
+ return eventPath.includes(channelListElement);
23918
+ };
23526
23919
  const handleClickOutside = (event) => {
23527
23920
  if (typeof window !== "undefined" && window.innerWidth >= MOBILE_NAV_BREAKPOINT) {
23528
23921
  return;
23529
23922
  }
23530
- if (closeMobileNav && channelListRef.current && !channelListRef.current.contains(event.target) && navOpen) {
23923
+ if (closeMobileNav && channelListRef.current && !isClickInsideChannelList(event) && navOpen) {
23531
23924
  closeMobileNav();
23532
23925
  }
23533
23926
  };
@@ -24657,7 +25050,7 @@ const UnMemoizedLoadMoreButton = ({
24657
25050
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__load-more-button", children: /* @__PURE__ */ jsxRuntime.jsx(
24658
25051
  WithAudioPlayback.Button,
24659
25052
  {
24660
- appearance: "outline",
25053
+ appearance: "ghost",
24661
25054
  "aria-label": t("aria/Load More Channels"),
24662
25055
  "data-testid": "load-more-button",
24663
25056
  disabled: loading,
@@ -25265,6 +25658,36 @@ const useArchiveActionButtonBehavior = () => {
25265
25658
  };
25266
25659
  };
25267
25660
  const defaultChannelActionSet = [
25661
+ {
25662
+ // eslint-disable-next-line react/display-name
25663
+ Component: React.forwardRef((_, ref) => {
25664
+ const { channel } = useChannelListItemContext();
25665
+ const dialogId2 = ChannelListItemActionButtons.getDialogId({
25666
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
25667
+ channelId: channel.id
25668
+ });
25669
+ const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId2 });
25670
+ const dialogIsOpen = useDialogIsOpen(dialogId2, dialogManager?.id);
25671
+ return /* @__PURE__ */ jsxRuntime.jsx(
25672
+ WithAudioPlayback.Button,
25673
+ {
25674
+ appearance: "ghost",
25675
+ "aria-expanded": dialogIsOpen,
25676
+ "aria-pressed": dialogIsOpen,
25677
+ circular: true,
25678
+ onClick: (e) => {
25679
+ e.stopPropagation();
25680
+ dialog.toggle();
25681
+ },
25682
+ ref,
25683
+ size: "sm",
25684
+ variant: "secondary",
25685
+ children: /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconDotGrid1x3Horizontal, {})
25686
+ }
25687
+ );
25688
+ }),
25689
+ placement: "quick-dropdown-toggle"
25690
+ },
25268
25691
  {
25269
25692
  Component() {
25270
25693
  const behaviorProps = useArchiveActionButtonBehavior();
@@ -25397,7 +25820,7 @@ const defaultChannelActionSet = [
25397
25820
  const membership = useChannelMembershipState(channel);
25398
25821
  const dialogId2 = ChannelListItemActionButtons.getDialogId(
25399
25822
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
25400
- channel.id
25823
+ { channelId: channel.id }
25401
25824
  );
25402
25825
  const { dialog } = useDialogOnNearestManager({ id: dialogId2 });
25403
25826
  const [inProgress, setInProgress] = React.useState(false);
@@ -25499,59 +25922,23 @@ const useBaseChannelActionSetFilter = (channelActionSet) => {
25499
25922
  const connectedUserIsMember = typeof membership.user !== "undefined";
25500
25923
  const ownCapabilities = channel.data?.own_capabilities;
25501
25924
  return React.useMemo(() => {
25502
- const filtered = channelActionSet.filter(
25503
- (action) => tsPattern.match({
25504
- action,
25505
- connectedUserIsMember,
25506
- isDirectMessageChannel,
25507
- memberCount,
25508
- ownCapabilities
25509
- }).returnType().with(
25510
- {
25511
- action: { connectedUserIsMember: true, placement: "quick", type: "archive" },
25512
- isDirectMessageChannel: true
25513
- },
25514
- {
25515
- action: {
25516
- connectedUserIsMember: true,
25517
- placement: "dropdown",
25518
- type: "archive"
25519
- },
25520
- isDirectMessageChannel: false
25521
- },
25522
- {
25523
- action: { placement: "dropdown", type: "mute" },
25524
- isDirectMessageChannel: true,
25525
- ownCapabilities: tsPattern.P.when(
25526
- (capabilities) => capabilities?.includes("mute-channel")
25527
- )
25528
- },
25529
- {
25530
- action: { placement: "quick", type: "mute" },
25531
- isDirectMessageChannel: false,
25532
- ownCapabilities: tsPattern.P.when(
25533
- (capabilities) => capabilities?.includes("mute-channel")
25534
- )
25535
- },
25536
- {
25537
- action: { type: "ban" },
25538
- memberCount: tsPattern.P.number.gt(0).and(tsPattern.P.number.lte(2)),
25539
- ownCapabilities: tsPattern.P.when(
25540
- (capabilities) => capabilities?.includes("ban-channel-members")
25541
- )
25542
- },
25543
- {
25544
- action: { type: "leave" },
25545
- ownCapabilities: tsPattern.P.when(
25546
- (capabilities) => capabilities?.includes("leave-channel")
25547
- )
25548
- },
25549
- {
25550
- action: { connectedUserIsMember: true, type: "pin" }
25551
- },
25552
- () => true
25553
- ).otherwise(() => false)
25554
- );
25925
+ const filtered = channelActionSet.filter((action) => {
25926
+ if (action.placement === "quick-dropdown-toggle") return true;
25927
+ switch (action.type) {
25928
+ case "archive":
25929
+ return connectedUserIsMember && (action.placement === "quick" && isDirectMessageChannel || action.placement === "dropdown" && !isDirectMessageChannel);
25930
+ case "mute":
25931
+ return ownCapabilities?.includes("mute-channel") && (action.placement === "dropdown" && isDirectMessageChannel || action.placement === "quick" && !isDirectMessageChannel);
25932
+ case "ban":
25933
+ return memberCount > 0 && memberCount <= 2 && ownCapabilities?.includes("ban-channel-members");
25934
+ case "leave":
25935
+ return ownCapabilities?.includes("leave-channel");
25936
+ case "pin":
25937
+ return connectedUserIsMember;
25938
+ default:
25939
+ return true;
25940
+ }
25941
+ });
25555
25942
  return filtered;
25556
25943
  }, [
25557
25944
  channelActionSet,
@@ -25562,17 +25949,18 @@ const useBaseChannelActionSetFilter = (channelActionSet) => {
25562
25949
  ]);
25563
25950
  };
25564
25951
  const ChannelListItemActionButtons = () => {
25952
+ const { ContextMenu: ContextMenuComponent = ContextMenu } = WithAudioPlayback.useComponentContext();
25565
25953
  const { channel } = useChannelListItemContext();
25566
25954
  const [referenceElement, setReferenceElement] = React.useState(null);
25567
- const dialogId2 = ChannelListItemActionButtons.getDialogId(
25955
+ const dialogId2 = ChannelListItemActionButtons.getDialogId({
25568
25956
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
25569
- channel.id
25570
- );
25957
+ channelId: channel.id
25958
+ });
25571
25959
  const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId2 });
25572
25960
  const dialogIsOpen = useDialogIsOpen(dialogId2, dialogManager?.id);
25573
25961
  const filteredActionSet = useBaseChannelActionSetFilter(defaultChannelActionSet);
25574
- const splitActionSet = useSplitActionSet(filteredActionSet);
25575
- if (splitActionSet.quickActionSet.length + splitActionSet.dropdownActionSet.length === 0) {
25962
+ const { dropdownActionSet, quickActionSet, quickDropdownToggleAction } = useSplitActionSet(filteredActionSet);
25963
+ if (quickActionSet.length + dropdownActionSet.length === 0) {
25576
25964
  return null;
25577
25965
  }
25578
25966
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -25582,26 +25970,10 @@ const ChannelListItemActionButtons = () => {
25582
25970
  "str-chat__channel-list-item__action-buttons--active": dialogIsOpen
25583
25971
  }),
25584
25972
  children: [
25585
- splitActionSet.dropdownActionSet.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
25586
- WithAudioPlayback.Button,
25587
- {
25588
- appearance: "ghost",
25589
- "aria-expanded": dialogIsOpen,
25590
- "aria-pressed": dialogIsOpen,
25591
- circular: true,
25592
- onClick: (e) => {
25593
- e.stopPropagation();
25594
- dialog.toggle();
25595
- },
25596
- ref: setReferenceElement,
25597
- size: "sm",
25598
- variant: "secondary",
25599
- children: /* @__PURE__ */ jsxRuntime.jsx(WithAudioPlayback.IconDotGrid1x3Horizontal, {})
25600
- }
25601
- ),
25602
- splitActionSet.quickActionSet.map(({ Component, type }) => /* @__PURE__ */ jsxRuntime.jsx(Component, {}, type)),
25973
+ quickDropdownToggleAction && dropdownActionSet.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(quickDropdownToggleAction.Component, { ref: setReferenceElement }),
25974
+ quickActionSet.map(({ Component, type }) => /* @__PURE__ */ jsxRuntime.jsx(Component, {}, type)),
25603
25975
  /* @__PURE__ */ jsxRuntime.jsx(
25604
- ContextMenu,
25976
+ ContextMenuComponent,
25605
25977
  {
25606
25978
  className: "str-chat__channel-list-item__action-buttons-context-menu",
25607
25979
  dialogManagerId: dialogManager?.id,
@@ -25611,14 +25983,14 @@ const ChannelListItemActionButtons = () => {
25611
25983
  referenceElement,
25612
25984
  tabIndex: -1,
25613
25985
  trapFocus: true,
25614
- children: splitActionSet.dropdownActionSet.map(({ Component, type }) => /* @__PURE__ */ jsxRuntime.jsx(Component, {}, type))
25986
+ children: dropdownActionSet.map(({ Component, type }) => /* @__PURE__ */ jsxRuntime.jsx(Component, {}, type))
25615
25987
  }
25616
25988
  )
25617
25989
  ]
25618
25990
  }
25619
25991
  );
25620
25992
  };
25621
- ChannelListItemActionButtons.getDialogId = (channelId) => `channel-action-buttons-${channelId}`;
25993
+ ChannelListItemActionButtons.getDialogId = ({ channelId }) => `channel-action-buttons-${channelId}`;
25622
25994
  ChannelListItemActionButtons.displayName = "ChannelListItemActionButtons";
25623
25995
  function ChannelListItemTimestamp({ lastMessage }) {
25624
25996
  const { t, tDateTimeParser } = WithAudioPlayback.useTranslationContext("ChannelListItemTimestamp");
@@ -26944,7 +27316,7 @@ const VoiceRecordingPlayerUI = ({ audioPlayer }) => {
26944
27316
  secondsElapsed
26945
27317
  } = WithAudioPlayback.useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
26946
27318
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootClassName, "data-testid": "voice-recording-widget", children: [
26947
- /* @__PURE__ */ jsxRuntime.jsx(PlayButton, { isPlaying: !!isPlaying, onClick: audioPlayer.togglePlay }),
27319
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-attachment__voice-recording-widget__play-button-container", children: /* @__PURE__ */ jsxRuntime.jsx(PlayButton, { isPlaying: !!isPlaying, onClick: audioPlayer.togglePlay }) }),
26948
27320
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-attachment__voice-recording-widget__metadata", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "str-chat__message-attachment__voice-recording-widget__audio-state", children: [
26949
27321
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "str-chat__message-attachment__voice-recording-widget__timer", children: durationSeconds ? /* @__PURE__ */ jsxRuntime.jsx(
26950
27322
  DurationDisplay,
@@ -27353,23 +27725,44 @@ const FileAttachment = ({ attachment }) => {
27353
27725
  );
27354
27726
  };
27355
27727
  const Giphy = ({ attachment }) => {
27356
- const { giphyVersion: giphyVersionName } = WithAudioPlayback.useChannelStateContext();
27728
+ const { giphyVersion: giphyVersionName, imageAttachmentSizeHandler } = WithAudioPlayback.useChannelStateContext();
27357
27729
  const { BaseImage: BaseImage$1 = BaseImage } = WithAudioPlayback.useComponentContext();
27358
27730
  const { t } = WithAudioPlayback.useTranslationContext();
27359
27731
  const usesDefaultBaseImage = BaseImage$1 === BaseImage;
27732
+ const imageElement = React.useRef(null);
27733
+ const [attachmentConfiguration, setAttachmentConfiguration] = React.useState(void 0);
27360
27734
  const imageDescriptors = React.useMemo(
27361
27735
  () => toGalleryItemDescriptors(attachment, { giphyVersionName }),
27362
27736
  [attachment, giphyVersionName]
27363
27737
  );
27364
- if (!imageDescriptors?.imageUrl) return null;
27365
- const { alt, dimensions, imageUrl, title } = imageDescriptors;
27738
+ const alt = imageDescriptors && imageDescriptors.alt;
27739
+ const dimensions = imageDescriptors && imageDescriptors.dimensions;
27740
+ const imageUrl = imageDescriptors && imageDescriptors.imageUrl;
27741
+ const title = imageDescriptors && imageDescriptors.title;
27742
+ const resolvedImageUrl = attachmentConfiguration?.url || imageUrl;
27743
+ const imageStyleVariables = React.useMemo(() => {
27744
+ const originalHeight = Number(dimensions?.height);
27745
+ const originalWidth = Number(dimensions?.width);
27746
+ return {
27747
+ "--original-height": String(originalHeight > 1 ? originalHeight : 1e6),
27748
+ "--original-width": String(originalWidth > 1 ? originalWidth : 1e6)
27749
+ };
27750
+ }, [dimensions?.height, dimensions?.width]);
27751
+ React.useLayoutEffect(() => {
27752
+ if (!imageElement.current || !imageAttachmentSizeHandler) return;
27753
+ const config = imageAttachmentSizeHandler(attachment, imageElement.current);
27754
+ setAttachmentConfiguration(config);
27755
+ }, [attachment, imageAttachmentSizeHandler]);
27756
+ if (!imageUrl) return null;
27366
27757
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx(`str-chat__message-attachment-giphy`), children: [
27367
27758
  /* @__PURE__ */ jsxRuntime.jsx(
27368
27759
  BaseImage$1,
27369
27760
  {
27370
27761
  alt: alt ?? title ?? t("User uploaded content"),
27371
27762
  height: dimensions?.height,
27372
- src: imageUrl,
27763
+ ref: imageElement,
27764
+ src: resolvedImageUrl,
27765
+ style: imageStyleVariables,
27373
27766
  width: dimensions?.width,
27374
27767
  ...usesDefaultBaseImage ? { showDownloadButtonOnError: false } : {}
27375
27768
  }
@@ -28266,6 +28659,7 @@ exports.ContextMenu = ContextMenu;
28266
28659
  exports.ContextMenuBackButton = ContextMenuBackButton;
28267
28660
  exports.ContextMenuBody = ContextMenuBody;
28268
28661
  exports.ContextMenuButton = ContextMenuButton;
28662
+ exports.ContextMenuContent = ContextMenuContent;
28269
28663
  exports.ContextMenuHeader = ContextMenuHeader;
28270
28664
  exports.ContextMenuRoot = ContextMenuRoot;
28271
28665
  exports.CooldownTimer = CooldownTimer;
@@ -28338,6 +28732,7 @@ exports.MessageListContextProvider = MessageListContextProvider;
28338
28732
  exports.MessageProvider = MessageProvider;
28339
28733
  exports.MessageReactions = MessageReactions;
28340
28734
  exports.MessageReactionsDetail = MessageReactionsDetail;
28735
+ exports.MessageReactionsDetailLoadingIndicator = MessageReactionsDetailLoadingIndicator;
28341
28736
  exports.MessageRepliesCountButton = MessageRepliesCountButton;
28342
28737
  exports.MessageSearchResultItem = MessageSearchResultItem;
28343
28738
  exports.MessageStatus = MessageStatus;