stream-chat-react 13.2.3 → 13.4.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.
Files changed (97) hide show
  1. package/dist/components/Attachment/Attachment.d.ts +5 -3
  2. package/dist/components/Attachment/Attachment.js +12 -5
  3. package/dist/components/Attachment/AttachmentContainer.d.ts +4 -3
  4. package/dist/components/Attachment/AttachmentContainer.js +5 -1
  5. package/dist/components/Attachment/Geolocation.d.ts +13 -0
  6. package/dist/components/Attachment/Geolocation.js +34 -0
  7. package/dist/components/Attachment/icons.d.ts +2 -0
  8. package/dist/components/Attachment/icons.js +5 -0
  9. package/dist/components/Attachment/index.d.ts +3 -1
  10. package/dist/components/Attachment/index.js +3 -1
  11. package/dist/components/Attachment/utils.d.ts +4 -1
  12. package/dist/components/Channel/Channel.d.ts +1 -1
  13. package/dist/components/Channel/Channel.js +2 -0
  14. package/dist/components/ChannelPreview/utils.js +3 -0
  15. package/dist/components/Chat/hooks/useChat.js +1 -1
  16. package/dist/components/Dialog/DialogAnchor.d.ts +3 -2
  17. package/dist/components/Dialog/DialogAnchor.js +7 -2
  18. package/dist/components/Form/Dropdown.d.ts +14 -0
  19. package/dist/components/Form/Dropdown.js +49 -0
  20. package/dist/components/Form/SwitchField.js +3 -1
  21. package/dist/components/Location/ShareLocationDialog.d.ts +18 -0
  22. package/dist/components/Location/ShareLocationDialog.js +139 -0
  23. package/dist/components/Location/hooks/useLiveLocationSharingManager.d.ts +18 -0
  24. package/dist/components/Location/hooks/useLiveLocationSharingManager.js +57 -0
  25. package/dist/components/Location/index.d.ts +1 -0
  26. package/dist/components/Location/index.js +1 -0
  27. package/dist/components/Message/MessageSimple.js +6 -1
  28. package/dist/components/Message/index.d.ts +3 -1
  29. package/dist/components/Message/index.js +3 -1
  30. package/dist/components/Message/renderText/renderText.d.ts +1 -1
  31. package/dist/components/Message/renderText/renderText.js +1 -1
  32. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.d.ts +3 -1
  33. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.js +35 -26
  34. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.d.ts +13 -0
  35. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.js +25 -0
  36. package/dist/components/MessageInput/AttachmentPreviewList/index.d.ts +1 -0
  37. package/dist/components/MessageInput/AttachmentSelector.d.ts +2 -1
  38. package/dist/components/MessageInput/AttachmentSelector.js +34 -12
  39. package/dist/components/MessageInput/MessageInput.d.ts +3 -1
  40. package/dist/components/MessageInput/MessageInput.js +7 -3
  41. package/dist/components/MessageInput/hooks/index.d.ts +1 -0
  42. package/dist/components/MessageInput/hooks/index.js +1 -0
  43. package/dist/components/MessageInput/hooks/useAttachmentsForPreview.d.ts +17 -0
  44. package/dist/components/MessageInput/hooks/useAttachmentsForPreview.js +22 -0
  45. package/dist/components/MessageInput/index.d.ts +1 -1
  46. package/dist/components/Modal/Modal.d.ts +8 -3
  47. package/dist/components/Modal/Modal.js +19 -8
  48. package/dist/components/Poll/PollActions/AddCommentForm.js +8 -0
  49. package/dist/components/Poll/PollActions/SuggestPollOptionForm.js +6 -3
  50. package/dist/components/Poll/PollCreationDialog/PollCreationDialogControls.js +4 -1
  51. package/dist/components/TextareaComposer/SuggestionList/SuggestionListItem.js +4 -1
  52. package/dist/components/index.d.ts +2 -1
  53. package/dist/components/index.js +2 -0
  54. package/dist/context/ComponentContext.d.ts +3 -0
  55. package/dist/css/v2/index.css +1 -1
  56. package/dist/css/v2/index.layout.css +1 -1
  57. package/dist/experimental/index.browser.cjs +11 -0
  58. package/dist/experimental/index.browser.cjs.map +2 -2
  59. package/dist/experimental/index.node.cjs +11 -0
  60. package/dist/experimental/index.node.cjs.map +2 -2
  61. package/dist/i18n/Streami18n.d.ts +21 -0
  62. package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.js +2 -0
  63. package/dist/i18n/TranslationBuilder/notifications/pollVoteCountTrespass.d.ts +3 -0
  64. package/dist/i18n/TranslationBuilder/notifications/pollVoteCountTrespass.js +1 -0
  65. package/dist/i18n/de.json +22 -1
  66. package/dist/i18n/en.json +22 -1
  67. package/dist/i18n/es.json +22 -1
  68. package/dist/i18n/fr.json +22 -1
  69. package/dist/i18n/hi.json +22 -1
  70. package/dist/i18n/it.json +22 -1
  71. package/dist/i18n/ja.json +22 -1
  72. package/dist/i18n/ko.json +22 -1
  73. package/dist/i18n/nl.json +22 -1
  74. package/dist/i18n/pt.json +22 -1
  75. package/dist/i18n/ru.json +22 -1
  76. package/dist/i18n/tr.json +22 -1
  77. package/dist/index.browser.cjs +2610 -1727
  78. package/dist/index.browser.cjs.map +4 -4
  79. package/dist/index.node.cjs +2622 -1727
  80. package/dist/index.node.cjs.map +4 -4
  81. package/dist/scss/v2/AttachmentList/AttachmentList-layout.scss +50 -0
  82. package/dist/scss/v2/AttachmentList/AttachmentList-theme.scss +56 -0
  83. package/dist/scss/v2/AttachmentPreviewList/AttachmentPreviewList-layout.scss +3 -0
  84. package/dist/scss/v2/AttachmentPreviewList/AttachmentPreviewList-theme.scss +11 -0
  85. package/dist/scss/v2/Dialog/Dialog-layout.scss +1 -2
  86. package/dist/scss/v2/Form/Form-layout.scss +40 -0
  87. package/dist/scss/v2/Form/Form-theme.scss +62 -0
  88. package/dist/scss/v2/Location/Location-layout.scss +52 -0
  89. package/dist/scss/v2/Location/Location-theme.scss +32 -0
  90. package/dist/scss/v2/MessageInput/MessageInput-theme.scss +7 -0
  91. package/dist/scss/v2/Modal/Modal-layout.scss +2 -0
  92. package/dist/scss/v2/Poll/Poll-layout.scss +0 -35
  93. package/dist/scss/v2/Poll/Poll-theme.scss +0 -28
  94. package/dist/scss/v2/_icons.scss +1 -0
  95. package/dist/scss/v2/index.layout.scss +1 -0
  96. package/dist/scss/v2/index.scss +1 -0
  97. package/package.json +6 -6
@@ -0,0 +1,57 @@
1
+ import { LiveLocationManager } from 'stream-chat';
2
+ import { useEffect, useMemo } from 'react';
3
+ const isMobile = () => /Mobi/i.test(navigator.userAgent);
4
+ /**
5
+ * Checks whether the current browser is Safari.
6
+ */
7
+ export const isSafari = () => {
8
+ if (typeof navigator === 'undefined')
9
+ return false;
10
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
11
+ };
12
+ /**
13
+ * Checks whether the current browser is Firefox.
14
+ */
15
+ export const isFirefox = () => {
16
+ if (typeof navigator === 'undefined')
17
+ return false;
18
+ return navigator.userAgent?.includes('Firefox');
19
+ };
20
+ /**
21
+ * Checks whether the current browser is Google Chrome.
22
+ */
23
+ export const isChrome = () => {
24
+ if (typeof navigator === 'undefined')
25
+ return false;
26
+ return navigator.userAgent?.includes('Chrome');
27
+ };
28
+ const browser = () => {
29
+ if (isChrome())
30
+ return 'chrome';
31
+ if (isFirefox())
32
+ return 'firefox';
33
+ if (isSafari())
34
+ return 'safari';
35
+ return 'other';
36
+ };
37
+ export const useLiveLocationSharingManager = ({ client, getDeviceId, watchLocation, }) => {
38
+ const manager = useMemo(() => {
39
+ if (!client)
40
+ return null;
41
+ return new LiveLocationManager({
42
+ client,
43
+ getDeviceId: getDeviceId ??
44
+ (() => `web-${isMobile() ? 'mobile' : 'desktop'}-${browser()}-${client.userID}`),
45
+ watchLocation,
46
+ });
47
+ }, [client, getDeviceId, watchLocation]);
48
+ useEffect(() => {
49
+ if (!manager)
50
+ return;
51
+ manager.init();
52
+ return () => {
53
+ manager.unregisterSubscriptions();
54
+ };
55
+ }, [manager]);
56
+ return manager;
57
+ };
@@ -0,0 +1 @@
1
+ export * from './ShareLocationDialog';
@@ -0,0 +1 @@
1
+ export * from './ShareLocationDialog';
@@ -39,6 +39,11 @@ const MessageSimpleWithContext = (props) => {
39
39
  const hasAttachment = messageHasAttachments(message);
40
40
  const hasReactions = messageHasReactions(message);
41
41
  const isAIGenerated = useMemo(() => isMessageAIGenerated?.(message), [isMessageAIGenerated, message]);
42
+ const finalAttachments = useMemo(() => !message.shared_location && !message.attachments
43
+ ? []
44
+ : !message.shared_location
45
+ ? message.attachments
46
+ : [message.shared_location, ...(message.attachments ?? [])], [message]);
42
47
  if (isDateSeparatorMessage(message)) {
43
48
  return null;
44
49
  }
@@ -92,7 +97,7 @@ const MessageSimpleWithContext = (props) => {
92
97
  React.createElement("div", { className: 'str-chat__message-reactions-host' }, hasReactions && React.createElement(ReactionsList, { reverse: true })),
93
98
  React.createElement("div", { className: 'str-chat__message-bubble' },
94
99
  poll && React.createElement(Poll, { poll: poll }),
95
- message.attachments?.length && !message.quoted_message ? (React.createElement(Attachment, { actionHandler: handleAction, attachments: message.attachments })) : null,
100
+ finalAttachments?.length && !message.quoted_message ? (React.createElement(Attachment, { actionHandler: handleAction, attachments: finalAttachments })) : null,
96
101
  isAIGenerated ? (React.createElement(StreamedMessageText, { message: message, renderText: renderText })) : (React.createElement(MessageText, { message: message, renderText: renderText })),
97
102
  React.createElement(MessageErrorIcon, null))),
98
103
  showReplyCountButton && (React.createElement(MessageRepliesCountButton, { onClick: handleOpenThread, reply_count: message.reply_count })),
@@ -2,7 +2,9 @@ export * from './FixedHeightMessage';
2
2
  export * from './hooks';
3
3
  export * from './icons';
4
4
  export * from './Message';
5
+ export * from './MessageBlocked';
5
6
  export * from './MessageDeleted';
7
+ export * from './MessageEditedTimestamp';
6
8
  export * from './MessageIsThreadReplyInChannelButtonIndicator';
7
9
  export * from './MessageOptions';
8
10
  export * from './MessageRepliesCountButton';
@@ -13,7 +15,7 @@ export * from './MessageTimestamp';
13
15
  export * from './QuotedMessage';
14
16
  export * from './ReminderNotification';
15
17
  export * from './renderText';
18
+ export * from './StreamedMessageText';
16
19
  export * from './types';
17
20
  export * from './utils';
18
- export * from './StreamedMessageText';
19
21
  export type { TimestampProps } from './Timestamp';
@@ -2,7 +2,9 @@ export * from './FixedHeightMessage';
2
2
  export * from './hooks';
3
3
  export * from './icons';
4
4
  export * from './Message';
5
+ export * from './MessageBlocked';
5
6
  export * from './MessageDeleted';
7
+ export * from './MessageEditedTimestamp';
6
8
  export * from './MessageIsThreadReplyInChannelButtonIndicator';
7
9
  export * from './MessageOptions';
8
10
  export * from './MessageRepliesCountButton';
@@ -13,6 +15,6 @@ export * from './MessageTimestamp';
13
15
  export * from './QuotedMessage';
14
16
  export * from './ReminderNotification';
15
17
  export * from './renderText';
18
+ export * from './StreamedMessageText';
16
19
  export * from './types';
17
20
  export * from './utils';
18
- export * from './StreamedMessageText';
@@ -16,4 +16,4 @@ export type RenderTextOptions = {
16
16
  getRehypePlugins?: RenderTextPluginConfigurator;
17
17
  getRemarkPlugins?: RenderTextPluginConfigurator;
18
18
  };
19
- export declare const renderText: (text?: string, mentionedUsers?: UserResponse[], { allowedTagNames, customMarkDownRenderers, getRehypePlugins, getRemarkPlugins, }?: RenderTextOptions) => React.JSX.Element | null | undefined;
19
+ export declare const renderText: (text?: string, mentionedUsers?: UserResponse[], { allowedTagNames, customMarkDownRenderers, getRehypePlugins, getRemarkPlugins, }?: RenderTextOptions) => React.JSX.Element | null;
@@ -83,7 +83,7 @@ export const renderText = (text, mentionedUsers, { allowedTagNames = defaultAllo
83
83
  return strippedHref.includes(strippedText) || strippedText.includes(strippedHref);
84
84
  });
85
85
  if (noParsingNeeded.length > 0 || linkIsInBlock)
86
- return;
86
+ continue;
87
87
  try {
88
88
  // special case for mentions:
89
89
  // it could happen that a user's name matches with an e-mail format pattern.
@@ -4,12 +4,14 @@ import type { UnsupportedAttachmentPreviewProps } from './UnsupportedAttachmentP
4
4
  import type { VoiceRecordingPreviewProps } from './VoiceRecordingPreview';
5
5
  import type { FileAttachmentPreviewProps } from './FileAttachmentPreview';
6
6
  import type { ImageAttachmentPreviewProps } from './ImageAttachmentPreview';
7
+ import { type GeolocationPreviewProps } from './GeolocationPreview';
7
8
  export type AttachmentPreviewListProps = {
8
9
  AudioAttachmentPreview?: ComponentType<FileAttachmentPreviewProps>;
9
10
  FileAttachmentPreview?: ComponentType<FileAttachmentPreviewProps>;
11
+ GeolocationPreview?: ComponentType<GeolocationPreviewProps>;
10
12
  ImageAttachmentPreview?: ComponentType<ImageAttachmentPreviewProps>;
11
13
  UnsupportedAttachmentPreview?: ComponentType<UnsupportedAttachmentPreviewProps>;
12
14
  VideoAttachmentPreview?: ComponentType<FileAttachmentPreviewProps>;
13
15
  VoiceRecordingPreview?: ComponentType<VoiceRecordingPreviewProps>;
14
16
  };
15
- export declare const AttachmentPreviewList: ({ AudioAttachmentPreview, FileAttachmentPreview, ImageAttachmentPreview, UnsupportedAttachmentPreview, VideoAttachmentPreview, VoiceRecordingPreview, }: AttachmentPreviewListProps) => React.JSX.Element | null;
17
+ export declare const AttachmentPreviewList: ({ AudioAttachmentPreview, FileAttachmentPreview, GeolocationPreview, ImageAttachmentPreview, UnsupportedAttachmentPreview, VideoAttachmentPreview, VoiceRecordingPreview, }: AttachmentPreviewListProps) => React.JSX.Element | null;
@@ -4,34 +4,43 @@ import { UnsupportedAttachmentPreview as DefaultUnknownAttachmentPreview } from
4
4
  import { VoiceRecordingPreview as DefaultVoiceRecordingPreview } from './VoiceRecordingPreview';
5
5
  import { FileAttachmentPreview as DefaultFilePreview } from './FileAttachmentPreview';
6
6
  import { ImageAttachmentPreview as DefaultImagePreview } from './ImageAttachmentPreview';
7
- import { useAttachmentManagerState, useMessageComposer } from '../hooks';
8
- export const AttachmentPreviewList = ({ AudioAttachmentPreview = DefaultFilePreview, FileAttachmentPreview = DefaultFilePreview, ImageAttachmentPreview = DefaultImagePreview, UnsupportedAttachmentPreview = DefaultUnknownAttachmentPreview, VideoAttachmentPreview = DefaultFilePreview, VoiceRecordingPreview = DefaultVoiceRecordingPreview, }) => {
7
+ import { useAttachmentsForPreview, useMessageComposer } from '../hooks';
8
+ import { GeolocationPreview as DefaultGeolocationPreview, } from './GeolocationPreview';
9
+ export const AttachmentPreviewList = ({ AudioAttachmentPreview = DefaultFilePreview, FileAttachmentPreview = DefaultFilePreview, GeolocationPreview = DefaultGeolocationPreview, ImageAttachmentPreview = DefaultImagePreview, UnsupportedAttachmentPreview = DefaultUnknownAttachmentPreview, VideoAttachmentPreview = DefaultFilePreview, VoiceRecordingPreview = DefaultVoiceRecordingPreview, }) => {
9
10
  const messageComposer = useMessageComposer();
10
- const { attachments } = useAttachmentManagerState();
11
- if (!attachments.length)
11
+ // todo: we could also allow to attach poll to a message composition
12
+ const { attachments, location } = useAttachmentsForPreview();
13
+ if (!attachments.length && !location)
12
14
  return null;
13
15
  return (React.createElement("div", { className: 'str-chat__attachment-preview-list' },
14
- React.createElement("div", { className: 'str-chat__attachment-list-scroll-container', "data-testid": 'attachment-list-scroll-container' }, attachments.map((attachment) => {
15
- if (isScrapedContent(attachment))
16
+ React.createElement("div", { className: 'str-chat__attachment-list-scroll-container', "data-testid": 'attachment-list-scroll-container' },
17
+ location && (React.createElement(GeolocationPreview, { location: location,
18
+ // It is not possible to nullify shared_location field so we do not show a preview when editing
19
+ // to prevent a user from wanting to remove the location
20
+ remove: messageComposer.editedMessage
21
+ ? undefined
22
+ : messageComposer.locationComposer.initState })),
23
+ attachments.map((attachment) => {
24
+ if (isScrapedContent(attachment))
25
+ return null;
26
+ if (isLocalVoiceRecordingAttachment(attachment)) {
27
+ return (React.createElement(VoiceRecordingPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
28
+ }
29
+ else if (isLocalAudioAttachment(attachment)) {
30
+ return (React.createElement(AudioAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
31
+ }
32
+ else if (isLocalVideoAttachment(attachment)) {
33
+ return (React.createElement(VideoAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
34
+ }
35
+ else if (isLocalImageAttachment(attachment)) {
36
+ return (React.createElement(ImageAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.image_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
37
+ }
38
+ else if (isLocalFileAttachment(attachment)) {
39
+ return (React.createElement(FileAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
40
+ }
41
+ else if (isLocalAttachment(attachment)) {
42
+ return (React.createElement(UnsupportedAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
43
+ }
16
44
  return null;
17
- if (isLocalVoiceRecordingAttachment(attachment)) {
18
- return (React.createElement(VoiceRecordingPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
19
- }
20
- else if (isLocalAudioAttachment(attachment)) {
21
- return (React.createElement(AudioAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
22
- }
23
- else if (isLocalVideoAttachment(attachment)) {
24
- return (React.createElement(VideoAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
25
- }
26
- else if (isLocalImageAttachment(attachment)) {
27
- return (React.createElement(ImageAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.image_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
28
- }
29
- else if (isLocalFileAttachment(attachment)) {
30
- return (React.createElement(FileAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
31
- }
32
- else if (isLocalAttachment(attachment)) {
33
- return (React.createElement(UnsupportedAttachmentPreview, { attachment: attachment, handleRetry: messageComposer.attachmentManager.uploadAttachment, key: attachment.localMetadata.id, removeAttachments: messageComposer.attachmentManager.removeAttachments }));
34
- }
35
- return null;
36
- }))));
45
+ }))));
37
46
  };
@@ -0,0 +1,13 @@
1
+ import type { LiveLocationPreview, StaticLocationPreview } from 'stream-chat';
2
+ import type { ComponentType } from 'react';
3
+ import React from 'react';
4
+ type GeolocationPreviewImageProps = {
5
+ location: StaticLocationPreview | LiveLocationPreview;
6
+ };
7
+ export type GeolocationPreviewProps = {
8
+ location: StaticLocationPreview | LiveLocationPreview;
9
+ PreviewImage?: ComponentType<GeolocationPreviewImageProps>;
10
+ remove?: () => void;
11
+ };
12
+ export declare const GeolocationPreview: ({ location, PreviewImage, remove, }: GeolocationPreviewProps) => React.JSX.Element;
13
+ export {};
@@ -0,0 +1,25 @@
1
+ import { CloseIcon } from '../icons';
2
+ import React from 'react';
3
+ import { useTranslationContext } from '../../../context';
4
+ import { GeolocationIcon } from '../../Attachment/icons';
5
+ const GeolocationPreviewImage = () => (React.createElement("div", { className: 'str-chat__location-preview-image' },
6
+ React.createElement(GeolocationIcon, null)));
7
+ export const GeolocationPreview = ({ location, PreviewImage = GeolocationPreviewImage, remove, }) => {
8
+ const { t } = useTranslationContext();
9
+ return (React.createElement("div", { className: 'str-chat__location-preview', "data-testid": 'location-preview' },
10
+ React.createElement(PreviewImage, { location: location }),
11
+ remove && (React.createElement("button", { "aria-label": t('aria/Remove location attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'location-preview-item-delete-button', onClick: remove },
12
+ React.createElement(CloseIcon, null))),
13
+ React.createElement("div", { className: 'str-chat__attachment-preview-metadata' }, location.durationMs ? (React.createElement(React.Fragment, null,
14
+ React.createElement("div", { className: 'str-chat__attachment-preview-title', title: t('Shared live location') }, t('Live location')),
15
+ React.createElement("div", { className: 'str-chat__attachment-preview-subtitle' }, t('Live for {{duration}}', {
16
+ duration: t('duration/Share Location', {
17
+ milliseconds: location.durationMs,
18
+ }),
19
+ })))) : (React.createElement(React.Fragment, null,
20
+ React.createElement("div", { className: 'str-chat__attachment-preview-title', title: t('Current location') }, t('Current location')),
21
+ React.createElement("div", { className: 'str-chat__attachment-preview-subtitle' },
22
+ location.latitude,
23
+ ", ",
24
+ location.longitude))))));
25
+ };
@@ -1,5 +1,6 @@
1
1
  export * from './AttachmentPreviewList';
2
2
  export type { FileAttachmentPreviewProps } from './FileAttachmentPreview';
3
+ export type { GeolocationPreviewProps } from './GeolocationPreview';
3
4
  export type { ImageAttachmentPreviewProps } from './ImageAttachmentPreview';
4
5
  export type { UploadAttachmentPreviewProps as AttachmentPreviewProps } from './types';
5
6
  export type { UnsupportedAttachmentPreviewProps } from './UnsupportedAttachmentPreview';
@@ -9,11 +9,12 @@ export type AttachmentSelectorActionProps = {
9
9
  };
10
10
  export type AttachmentSelectorAction = {
11
11
  ActionButton: React.ComponentType<AttachmentSelectorActionProps>;
12
- type: 'uploadFile' | 'createPoll' | (string & {});
12
+ type: 'uploadFile' | 'createPoll' | 'addLocation' | (string & {});
13
13
  ModalContent?: React.ComponentType<AttachmentSelectorModalContentProps>;
14
14
  };
15
15
  export declare const DefaultAttachmentSelectorComponents: {
16
16
  File({ closeMenu }: AttachmentSelectorActionProps): React.JSX.Element;
17
+ Location({ closeMenu, openModalForAction }: AttachmentSelectorActionProps): React.JSX.Element;
17
18
  Poll({ closeMenu, openModalForAction }: AttachmentSelectorActionProps): React.JSX.Element;
18
19
  };
19
20
  export declare const defaultAttachmentSelectorActionSet: AttachmentSelectorAction[];
@@ -5,12 +5,15 @@ import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
5
5
  import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
6
6
  import { DialogMenuButton } from '../Dialog/DialogMenu';
7
7
  import { Modal } from '../Modal';
8
+ import { ShareLocationDialog as DefaultLocationDialog } from '../Location';
8
9
  import { PollCreationDialog as DefaultPollCreationDialog } from '../Poll';
9
10
  import { Portal } from '../Portal/Portal';
10
11
  import { UploadFileInput } from '../ReactFileUtilities';
11
- import { useChannelStateContext, useComponentContext, useMessageInputContext, useTranslationContext, } from '../../context';
12
+ import { useChannelStateContext, useComponentContext, useTranslationContext, } from '../../context';
12
13
  import { AttachmentSelectorContextProvider, useAttachmentSelectorContext, } from '../../context/AttachmentSelectorContext';
13
14
  import { useStableId } from '../UtilityComponents/useStableId';
15
+ import clsx from 'clsx';
16
+ import { useMessageComposer } from './hooks';
14
17
  export const SimpleAttachmentSelector = () => {
15
18
  const { AttachmentSelectorInitiationButtonContents, FileUploadIcon = DefaultUploadIcon, } = useComponentContext();
16
19
  const inputRef = useRef(null);
@@ -55,6 +58,13 @@ export const DefaultAttachmentSelectorComponents = {
55
58
  closeMenu();
56
59
  } }, t('File')));
57
60
  },
61
+ Location({ closeMenu, openModalForAction }) {
62
+ const { t } = useTranslationContext();
63
+ return (React.createElement(DialogMenuButton, { className: 'str-chat__attachment-selector-actions-menu__button str-chat__attachment-selector-actions-menu__add-location-button', onClick: () => {
64
+ openModalForAction('addLocation');
65
+ closeMenu();
66
+ } }, t('Location')));
67
+ },
58
68
  Poll({ closeMenu, openModalForAction }) {
59
69
  const { t } = useTranslationContext();
60
70
  return (React.createElement(DialogMenuButton, { className: 'str-chat__attachment-selector-actions-menu__button str-chat__attachment-selector-actions-menu__create-poll-button', onClick: () => {
@@ -69,33 +79,42 @@ export const defaultAttachmentSelectorActionSet = [
69
79
  ActionButton: DefaultAttachmentSelectorComponents.Poll,
70
80
  type: 'createPoll',
71
81
  },
82
+ {
83
+ ActionButton: DefaultAttachmentSelectorComponents.Location,
84
+ type: 'addLocation',
85
+ },
72
86
  ];
73
87
  const useAttachmentSelectorActionsFiltered = (original) => {
74
- const { PollCreationDialog = DefaultPollCreationDialog } = useComponentContext();
75
- const { channelCapabilities, channelConfig } = useChannelStateContext();
76
- const { isThreadInput } = useMessageInputContext();
88
+ const { PollCreationDialog = DefaultPollCreationDialog, ShareLocationDialog = DefaultLocationDialog, } = useComponentContext();
89
+ const { channelCapabilities } = useChannelStateContext();
90
+ const messageComposer = useMessageComposer();
77
91
  return original
78
92
  .filter((action) => {
79
- if (action.type === 'uploadFile' && !channelCapabilities['upload-file'])
80
- return false;
81
- if (action.type === 'createPoll' &&
82
- (!channelConfig?.polls || isThreadInput || !channelCapabilities['send-poll']))
83
- return false;
93
+ if (action.type === 'uploadFile')
94
+ return channelCapabilities['upload-file'];
95
+ if (action.type === 'createPoll')
96
+ return channelCapabilities['send-poll'] && !messageComposer.threadId;
97
+ if (action.type === 'addLocation') {
98
+ return messageComposer.config.location.enabled && !messageComposer.threadId;
99
+ }
84
100
  return true;
85
101
  })
86
102
  .map((action) => {
87
103
  if (action.type === 'createPoll' && !action.ModalContent) {
88
104
  return { ...action, ModalContent: PollCreationDialog };
89
105
  }
106
+ if (action.type === 'addLocation' && !action.ModalContent) {
107
+ return { ...action, ModalContent: ShareLocationDialog };
108
+ }
90
109
  return action;
91
110
  });
92
111
  };
93
112
  export const AttachmentSelector = ({ attachmentSelectorActionSet = defaultAttachmentSelectorActionSet, getModalPortalDestination, }) => {
94
113
  const { t } = useTranslationContext();
95
114
  const { channelCapabilities } = useChannelStateContext();
96
- const { isThreadInput } = useMessageInputContext();
115
+ const messageComposer = useMessageComposer();
97
116
  const actions = useAttachmentSelectorActionsFiltered(attachmentSelectorActionSet);
98
- const menuDialogId = `attachment-actions-menu${isThreadInput ? '-thread' : ''}`;
117
+ const menuDialogId = `attachment-actions-menu${messageComposer.threadId ? '-thread' : ''}`;
99
118
  const menuDialog = useDialog({ id: menuDialogId });
100
119
  const menuDialogIsOpen = useDialogIsOpen(menuDialogId);
101
120
  const [modalContentAction, setModalContentActionAction] = useState();
@@ -123,5 +142,8 @@ export const AttachmentSelector = ({ attachmentSelectorActionSet = defaultAttach
123
142
  React.createElement(DialogAnchor, { id: menuDialogId, placement: 'top-start', referenceElement: menuButtonRef.current, tabIndex: -1, trapFocus: true },
124
143
  React.createElement("div", { className: 'str-chat__attachment-selector-actions-menu str-chat__dialog-menu', "data-testid": 'attachment-selector-actions-menu' }, actions.map(({ ActionButton, type }) => (React.createElement(ActionButton, { closeMenu: menuDialog.close, key: `attachment-selector-item-${type}`, openModalForAction: openModal }))))),
125
144
  React.createElement(Portal, { getPortalDestination: getModalPortalDestination ?? getDefaultPortalDestination, isOpen: modalIsOpen },
126
- React.createElement(Modal, { className: 'str-chat__create-poll-modal', onClose: closeModal, open: modalIsOpen }, ModalContent && React.createElement(ModalContent, { close: closeModal }))))));
145
+ React.createElement(Modal, { className: clsx({
146
+ 'str-chat__create-poll-modal': modalContentAction?.type === 'createPoll',
147
+ 'str-chat__share-location-modal': modalContentAction?.type === 'addLocation',
148
+ }), onClose: closeModal, open: modalIsOpen }, ModalContent && React.createElement(ModalContent, { close: closeModal }))))));
127
149
  };
@@ -40,7 +40,9 @@ export type MessageInputProps = {
40
40
  hideSendButton?: boolean;
41
41
  /** Custom UI component handling how the message input is rendered, defaults to and accepts the same props as [MessageInputFlat](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/MessageInputFlat.tsx) */
42
42
  Input?: React.ComponentType<MessageInputProps>;
43
- /** Signals that the MessageInput is rendered in a message thread (Thread component) */
43
+ /** @deprecated use messageComposer.threadId to indicate, whether the message is composed within a thread context
44
+ * Signals that the MessageInput is rendered in a message thread (Thread component)
45
+ */
44
46
  isThreadInput?: boolean;
45
47
  /** Max number of rows the underlying `textarea` component is allowed to grow */
46
48
  maxRows?: number;
@@ -31,11 +31,14 @@ const MessageInputProvider = (props) => {
31
31
  !messageComposer.config.drafts.enabled)
32
32
  return;
33
33
  // get draft data for legacy thead composer
34
- messageComposer.channel.getDraft({ parent_id: threadId }).then(({ draft }) => {
34
+ messageComposer.channel
35
+ .getDraft({ parent_id: threadId })
36
+ .then(({ draft }) => {
35
37
  if (draft) {
36
38
  messageComposer.initState({ composition: draft });
37
39
  }
38
- });
40
+ })
41
+ .catch(console.error);
39
42
  }, [messageComposer]);
40
43
  useRegisterDropHandlers();
41
44
  return (React.createElement(MessageInputContextProvider, { value: messageInputContextValue }, props.children));
@@ -43,9 +46,10 @@ const MessageInputProvider = (props) => {
43
46
  const UnMemoizedMessageInput = (props) => {
44
47
  const { Input: PropInput } = props;
45
48
  const { Input: ContextInput } = useComponentContext('MessageInput');
49
+ const messageComposer = useMessageComposer();
46
50
  const id = useStableId();
47
51
  const Input = PropInput || ContextInput || MessageInputFlat;
48
- const dialogManagerId = props.isThreadInput
52
+ const dialogManagerId = messageComposer.threadId
49
53
  ? `message-input-dialog-manager-thread-${id}`
50
54
  : `message-input-dialog-manager-${id}`;
51
55
  return (React.createElement(DialogManagerProvider, { id: dialogManagerId },
@@ -1,4 +1,5 @@
1
1
  export * from './useAttachmentManagerState';
2
+ export * from './useAttachmentsForPreview';
2
3
  export * from './useCanCreatePoll';
3
4
  export * from './useCooldownTimer';
4
5
  export * from './useMessageInputControls';
@@ -1,4 +1,5 @@
1
1
  export * from './useAttachmentManagerState';
2
+ export * from './useAttachmentsForPreview';
2
3
  export * from './useCanCreatePoll';
3
4
  export * from './useCooldownTimer';
4
5
  export * from './useMessageInputControls';
@@ -0,0 +1,17 @@
1
+ export declare const useAttachmentsForPreview: () => {
2
+ attachments: import("stream-chat").LocalAttachment[];
3
+ location: import("stream-chat").StaticLocationPayload | import("stream-chat").LiveLocationPreview | null;
4
+ poll: {
5
+ id: string;
6
+ max_votes_allowed: string;
7
+ name: string;
8
+ options: import("stream-chat").PollComposerOption[];
9
+ allow_answers?: boolean | undefined;
10
+ allow_user_suggested_options?: boolean | undefined;
11
+ description?: string | undefined;
12
+ enforce_unique_vote?: boolean | undefined;
13
+ is_closed?: boolean | undefined;
14
+ user_id?: string | undefined;
15
+ voting_visibility?: import("stream-chat").VotingVisibility | undefined;
16
+ };
17
+ };
@@ -0,0 +1,22 @@
1
+ import { useMessageComposer } from './useMessageComposer';
2
+ import { useStateStore } from '../../../store';
3
+ const attachmentManagerStateSelector = (state) => ({
4
+ attachments: state.attachments,
5
+ });
6
+ const pollComposerStateSelector = (state) => ({
7
+ poll: state.data,
8
+ });
9
+ const locationComposerStateSelector = (state) => ({
10
+ location: state.location,
11
+ });
12
+ export const useAttachmentsForPreview = () => {
13
+ const { attachmentManager, locationComposer, pollComposer } = useMessageComposer();
14
+ const { attachments } = useStateStore(attachmentManager.state, attachmentManagerStateSelector);
15
+ const { poll } = useStateStore(pollComposer.state, pollComposerStateSelector);
16
+ const { location } = useStateStore(locationComposer.state, locationComposerStateSelector);
17
+ return {
18
+ attachments,
19
+ location,
20
+ poll,
21
+ };
22
+ };
@@ -1,6 +1,6 @@
1
1
  export * from './AttachmentSelector';
2
2
  export { AttachmentPreviewList } from './AttachmentPreviewList';
3
- export type { AttachmentPreviewListProps, FileAttachmentPreviewProps, ImageAttachmentPreviewProps, AttachmentPreviewProps, UnsupportedAttachmentPreviewProps, VoiceRecordingPreviewProps, } from './AttachmentPreviewList';
3
+ export type { AttachmentPreviewListProps, FileAttachmentPreviewProps, GeolocationPreviewProps, ImageAttachmentPreviewProps, AttachmentPreviewProps, UnsupportedAttachmentPreviewProps, VoiceRecordingPreviewProps, } from './AttachmentPreviewList';
4
4
  export * from './CooldownTimer';
5
5
  export * from './EditMessageForm';
6
6
  export * from './hooks';
@@ -1,11 +1,16 @@
1
- import type { PropsWithChildren } from 'react';
1
+ import { type PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
+ type CloseEvent = KeyboardEvent | React.KeyboardEvent | React.MouseEvent<HTMLButtonElement | HTMLDivElement>;
4
+ export type ModalCloseSource = 'overlay' | 'button' | 'escape';
3
5
  export type ModalProps = {
4
6
  /** If true, modal is opened or visible. */
5
7
  open: boolean;
6
8
  /** Custom class to be applied to the modal root div */
7
9
  className?: string;
8
10
  /** Callback handler for closing of modal. */
9
- onClose?: (event: React.KeyboardEvent | React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => void;
11
+ onClose?: (event: CloseEvent) => void;
12
+ /** Optional handler to intercept closing logic. Return false to prevent onClose. */
13
+ onCloseAttempt?: (source: ModalCloseSource, event: CloseEvent) => boolean;
10
14
  };
11
- export declare const Modal: ({ children, className, onClose, open, }: PropsWithChildren<ModalProps>) => React.JSX.Element | null;
15
+ export declare const Modal: ({ children, className, onClose, onCloseAttempt, open, }: PropsWithChildren<ModalProps>) => React.JSX.Element | null;
16
+ export {};
@@ -1,34 +1,45 @@
1
1
  import clsx from 'clsx';
2
+ import { useCallback } from 'react';
2
3
  import React, { useEffect, useRef } from 'react';
3
4
  import { FocusScope } from '@react-aria/focus';
4
5
  import { CloseIconRound } from './icons';
5
6
  import { useTranslationContext } from '../../context';
6
- export const Modal = ({ children, className, onClose, open, }) => {
7
+ export const Modal = ({ children, className, onClose, onCloseAttempt, open, }) => {
7
8
  const { t } = useTranslationContext('Modal');
8
9
  const innerRef = useRef(null);
9
- const closeRef = useRef(null);
10
+ const closeButtonRef = useRef(null);
11
+ const maybeClose = useCallback((source, event) => {
12
+ const allow = onCloseAttempt?.(source, event);
13
+ if (allow !== false) {
14
+ onClose?.(event);
15
+ }
16
+ }, [onClose, onCloseAttempt]);
10
17
  const handleClick = (event) => {
11
18
  const target = event.target;
12
- if (!innerRef.current || !closeRef.current)
19
+ if (!innerRef.current || !closeButtonRef.current)
13
20
  return;
14
- if (!innerRef.current.contains(target) || closeRef.current.contains(target))
15
- onClose?.(event);
21
+ if (closeButtonRef.current.contains(target)) {
22
+ maybeClose('button', event);
23
+ }
24
+ else if (!innerRef.current.contains(target)) {
25
+ maybeClose('overlay', event);
26
+ }
16
27
  };
17
28
  useEffect(() => {
18
29
  if (!open)
19
30
  return;
20
31
  const handleKeyDown = (event) => {
21
32
  if (event.key === 'Escape')
22
- onClose?.(event);
33
+ maybeClose('escape', event);
23
34
  };
24
35
  document.addEventListener('keydown', handleKeyDown);
25
36
  return () => document.removeEventListener('keydown', handleKeyDown);
26
- }, [onClose, open]);
37
+ }, [maybeClose, open]);
27
38
  if (!open)
28
39
  return null;
29
40
  return (React.createElement("div", { className: clsx('str-chat__modal str-chat__modal--open', className), onClick: handleClick },
30
41
  React.createElement(FocusScope, { autoFocus: true, contain: true },
31
- React.createElement("button", { className: 'str-chat__modal__close-button', ref: closeRef, title: t('Close') },
42
+ React.createElement("button", { className: 'str-chat__modal__close-button', ref: closeButtonRef, title: t('Close') },
32
43
  React.createElement(CloseIconRound, null)),
33
44
  React.createElement("div", { className: 'str-chat__modal__inner str-chat-react__modal__inner', ref: innerRef }, children))));
34
45
  };
@@ -19,6 +19,14 @@ export const AddCommentForm = ({ close, messageId }) => {
19
19
  type: 'text',
20
20
  value: ownAnswer?.answer_text ?? '',
21
21
  },
22
+ validator: (value) => {
23
+ const valueString = typeof value !== 'undefined' ? value.toString() : value;
24
+ const trimmedValue = valueString?.trim();
25
+ if (!trimmedValue) {
26
+ return new Error(t('This field cannot be empty or contain only spaces'));
27
+ }
28
+ return;
29
+ },
22
30
  },
23
31
  }, onSubmit: async (value) => {
24
32
  await poll.addAnswer(value.comment, messageId);
@@ -21,9 +21,12 @@ export const SuggestPollOptionForm = ({ close, messageId, }) => {
21
21
  value: '',
22
22
  },
23
23
  validator: (value) => {
24
- if (!value)
25
- return;
26
- const existingOption = options.find((option) => option.text === value.trim());
24
+ const valueString = typeof value !== 'undefined' ? value.toString() : value;
25
+ const trimmedValue = valueString?.trim();
26
+ if (!trimmedValue) {
27
+ return new Error(t('This field cannot be empty or contain only spaces'));
28
+ }
29
+ const existingOption = options.find((option) => option.text === trimmedValue);
27
30
  if (existingOption) {
28
31
  return new Error(t('Option already exists'));
29
32
  }
@@ -15,7 +15,10 @@ export const PollCreationDialogControls = ({ close, }) => {
15
15
  messageComposer
16
16
  .createPoll()
17
17
  .then(() => handleSubmitMessage())
18
- .then(close)
18
+ .then(() => {
19
+ messageComposer.pollComposer.initState();
20
+ close();
21
+ })
19
22
  .catch(console.error);
20
23
  }, type: 'submit' }, t('Create'))));
21
24
  };