stream-chat-react 13.2.2 → 13.3.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 (87) 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 +19 -11
  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 +4 -1
  29. package/dist/components/Message/index.js +4 -1
  30. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.d.ts +3 -1
  31. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.js +35 -26
  32. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.d.ts +13 -0
  33. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.js +25 -0
  34. package/dist/components/MessageInput/AttachmentSelector.d.ts +2 -1
  35. package/dist/components/MessageInput/AttachmentSelector.js +34 -12
  36. package/dist/components/MessageInput/MessageInput.d.ts +3 -1
  37. package/dist/components/MessageInput/MessageInput.js +7 -3
  38. package/dist/components/MessageInput/hooks/index.d.ts +1 -0
  39. package/dist/components/MessageInput/hooks/index.js +1 -0
  40. package/dist/components/MessageInput/hooks/useAttachmentsForPreview.d.ts +17 -0
  41. package/dist/components/MessageInput/hooks/useAttachmentsForPreview.js +22 -0
  42. package/dist/components/Poll/PollActions/AddCommentForm.js +8 -0
  43. package/dist/components/Poll/PollActions/SuggestPollOptionForm.js +6 -3
  44. package/dist/components/TextareaComposer/SuggestionList/SuggestionListItem.js +4 -1
  45. package/dist/components/index.d.ts +2 -1
  46. package/dist/components/index.js +2 -0
  47. package/dist/context/ComponentContext.d.ts +3 -0
  48. package/dist/css/v2/index.css +1 -1
  49. package/dist/css/v2/index.layout.css +1 -1
  50. package/dist/experimental/index.browser.cjs +11 -0
  51. package/dist/experimental/index.browser.cjs.map +2 -2
  52. package/dist/experimental/index.node.cjs +11 -0
  53. package/dist/experimental/index.node.cjs.map +2 -2
  54. package/dist/i18n/Streami18n.d.ts +20 -0
  55. package/dist/i18n/de.json +21 -1
  56. package/dist/i18n/en.json +21 -1
  57. package/dist/i18n/es.json +21 -1
  58. package/dist/i18n/fr.json +21 -1
  59. package/dist/i18n/hi.json +21 -1
  60. package/dist/i18n/it.json +21 -1
  61. package/dist/i18n/ja.json +21 -1
  62. package/dist/i18n/ko.json +21 -1
  63. package/dist/i18n/nl.json +21 -1
  64. package/dist/i18n/pt.json +21 -1
  65. package/dist/i18n/ru.json +21 -1
  66. package/dist/i18n/tr.json +21 -1
  67. package/dist/index.browser.cjs +1928 -1073
  68. package/dist/index.browser.cjs.map +4 -4
  69. package/dist/index.node.cjs +1941 -1073
  70. package/dist/index.node.cjs.map +4 -4
  71. package/dist/scss/v2/AttachmentList/AttachmentList-layout.scss +50 -0
  72. package/dist/scss/v2/AttachmentList/AttachmentList-theme.scss +56 -0
  73. package/dist/scss/v2/AttachmentPreviewList/AttachmentPreviewList-layout.scss +3 -0
  74. package/dist/scss/v2/AttachmentPreviewList/AttachmentPreviewList-theme.scss +11 -0
  75. package/dist/scss/v2/Dialog/Dialog-layout.scss +1 -2
  76. package/dist/scss/v2/Form/Form-layout.scss +40 -0
  77. package/dist/scss/v2/Form/Form-theme.scss +62 -0
  78. package/dist/scss/v2/Location/Location-layout.scss +52 -0
  79. package/dist/scss/v2/Location/Location-theme.scss +32 -0
  80. package/dist/scss/v2/MessageInput/MessageInput-theme.scss +7 -0
  81. package/dist/scss/v2/Modal/Modal-layout.scss +2 -0
  82. package/dist/scss/v2/Poll/Poll-layout.scss +0 -35
  83. package/dist/scss/v2/Poll/Poll-theme.scss +0 -28
  84. package/dist/scss/v2/_icons.scss +1 -0
  85. package/dist/scss/v2/index.layout.scss +1 -0
  86. package/dist/scss/v2/index.scss +1 -0
  87. package/package.json +4 -4
@@ -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,10 @@ 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';
8
+ export * from './MessageIsThreadReplyInChannelButtonIndicator';
6
9
  export * from './MessageOptions';
7
10
  export * from './MessageRepliesCountButton';
8
11
  export * from './MessageSimple';
@@ -12,7 +15,7 @@ export * from './MessageTimestamp';
12
15
  export * from './QuotedMessage';
13
16
  export * from './ReminderNotification';
14
17
  export * from './renderText';
18
+ export * from './StreamedMessageText';
15
19
  export * from './types';
16
20
  export * from './utils';
17
- export * from './StreamedMessageText';
18
21
  export type { TimestampProps } from './Timestamp';
@@ -2,7 +2,10 @@ 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';
8
+ export * from './MessageIsThreadReplyInChannelButtonIndicator';
6
9
  export * from './MessageOptions';
7
10
  export * from './MessageRepliesCountButton';
8
11
  export * from './MessageSimple';
@@ -12,6 +15,6 @@ export * from './MessageTimestamp';
12
15
  export * from './QuotedMessage';
13
16
  export * from './ReminderNotification';
14
17
  export * from './renderText';
18
+ export * from './StreamedMessageText';
15
19
  export * from './types';
16
20
  export * from './utils';
17
- export * from './StreamedMessageText';
@@ -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
+ };
@@ -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
+ };
@@ -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
  }
@@ -1,12 +1,15 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { useCallback, useLayoutEffect, useRef } from 'react';
3
3
  import { useMessageComposer } from '../../MessageInput';
4
+ import { useMessageInputContext } from '../../../context';
4
5
  export const SuggestionListItem = React.forwardRef(function SuggestionListItem({ className, component: Component, focused, item, onMouseEnter }, innerRef) {
5
6
  const { textComposer } = useMessageComposer();
7
+ const { textareaRef } = useMessageInputContext();
6
8
  const containerRef = useRef(null);
7
9
  const handleSelect = useCallback(() => {
8
10
  textComposer.handleSelect(item);
9
- }, [item, textComposer]);
11
+ textareaRef.current?.focus();
12
+ }, [item, textareaRef, textComposer]);
10
13
  useLayoutEffect(() => {
11
14
  if (!focused)
12
15
  return;
@@ -14,10 +14,11 @@ export * from './Gallery';
14
14
  export * from './InfiniteScrollPaginator';
15
15
  export * from './Loading';
16
16
  export * from './LoadMore';
17
+ export * from './Location';
17
18
  export * from './MediaRecorder';
18
19
  export * from './Message';
19
20
  export * from './MessageActions';
20
- export type { MessageBouncePromptProps } from './MessageBounce';
21
+ export * from './MessageBounce';
21
22
  export * from './MessageInput';
22
23
  export * from './MessageList';
23
24
  export * from './Modal';
@@ -14,9 +14,11 @@ export * from './Gallery';
14
14
  export * from './InfiniteScrollPaginator';
15
15
  export * from './Loading';
16
16
  export * from './LoadMore';
17
+ export * from './Location';
17
18
  export * from './MediaRecorder';
18
19
  export * from './Message';
19
20
  export * from './MessageActions';
21
+ export * from './MessageBounce';
20
22
  export * from './MessageInput';
21
23
  export * from './MessageList';
22
24
  export * from './Modal';
@@ -5,6 +5,7 @@ import type { SuggestionItemProps, SuggestionListProps } from '../components/Tex
5
5
  import type { SearchProps, SearchResultsPresearchProps, SearchSourceResultListProps } from '../experimental';
6
6
  import type { PropsWithChildrenOnly, UnknownType } from '../types/types';
7
7
  import type { StopAIGenerationButtonProps } from '../components/MessageInput/StopAIGenerationButton';
8
+ import type { ShareLocationDialogProps } from '../components/Location';
8
9
  export type ComponentContextValue = {
9
10
  /** Custom UI component to display a message attachment, defaults to and accepts same props as: [Attachment](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/Attachment.tsx) */
10
11
  Attachment?: React.ComponentType<AttachmentProps>;
@@ -143,6 +144,8 @@ export type ComponentContextValue = {
143
144
  SendButton?: React.ComponentType<SendButtonProps>;
144
145
  /** Custom UI component checkbox that indicates message to be sent to main channel, defaults to and accepts same props as: [SendToChannelCheckbox](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/SendToChannelCheckbox.tsx) */
145
146
  SendToChannelCheckbox?: React.ComponentType;
147
+ /** Custom UI component to render the location sharing dialog, defaults to and accepts same props as: [ShareLocationDialog](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Location/ShareLocationDialog.tsx) */
148
+ ShareLocationDialog?: React.ComponentType<ShareLocationDialogProps>;
146
149
  /** Custom UI component button for initiating audio recording, defaults to and accepts same props as: [StartRecordingAudioButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtons.tsx) */
147
150
  StartRecordingAudioButton?: React.ComponentType<StartRecordingAudioButtonProps>;
148
151
  StopAIGenerationButton?: React.ComponentType<StopAIGenerationButtonProps> | null;