stream-chat-react 12.0.0-rc.2 → 12.0.0-rc.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 (91) hide show
  1. package/README.md +10 -0
  2. package/dist/components/Attachment/components/WaveProgressBar.d.ts +3 -1
  3. package/dist/components/Attachment/components/WaveProgressBar.js +44 -9
  4. package/dist/components/Channel/channelState.js +1 -0
  5. package/dist/components/DateSeparator/DateSeparator.js +1 -1
  6. package/dist/components/EventComponent/EventComponent.js +1 -1
  7. package/dist/components/InfiniteScrollPaginator/InfiniteScroll.js +9 -3
  8. package/dist/components/MediaRecorder/classes/MediaRecorderController.d.ts +6 -7
  9. package/dist/components/MediaRecorder/classes/MediaRecorderController.js +0 -5
  10. package/dist/components/MediaRecorder/hooks/index.d.ts +1 -1
  11. package/dist/components/MediaRecorder/hooks/useMediaRecorder.d.ts +1 -2
  12. package/dist/components/MediaRecorder/hooks/useMediaRecorder.js +1 -1
  13. package/dist/components/MediaRecorder/index.d.ts +1 -0
  14. package/dist/components/MediaRecorder/transcode/index.d.ts +6 -5
  15. package/dist/components/MediaRecorder/transcode/index.js +5 -15
  16. package/dist/components/Message/MessageSimple.js +1 -1
  17. package/dist/components/Message/MessageTimestamp.d.ts +0 -1
  18. package/dist/components/Message/MessageTimestamp.js +0 -1
  19. package/dist/components/Message/Timestamp.d.ts +0 -1
  20. package/dist/components/Message/Timestamp.js +2 -3
  21. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.js +1 -1
  22. package/dist/components/Message/utils.js +2 -0
  23. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.js +23 -27
  24. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.d.ts +1 -0
  25. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.js +1 -1
  26. package/dist/components/MessageInput/AttachmentPreviewList/ImageAttachmentPreview.js +2 -1
  27. package/dist/components/MessageInput/MessageInput.d.ts +4 -6
  28. package/dist/components/MessageInput/MessageInputFlat.js +4 -7
  29. package/dist/components/MessageInput/hooks/useAttachments.d.ts +1 -5
  30. package/dist/components/MessageInput/hooks/useAttachments.js +65 -52
  31. package/dist/components/MessageInput/hooks/useCreateMessageInputContext.js +2 -19
  32. package/dist/components/MessageInput/hooks/useMessageInputState.d.ts +2 -35
  33. package/dist/components/MessageInput/hooks/useMessageInputState.js +2 -107
  34. package/dist/components/MessageInput/hooks/usePasteHandler.js +1 -3
  35. package/dist/components/MessageInput/hooks/useSubmitHandler.js +19 -71
  36. package/dist/components/MessageInput/hooks/utils.d.ts +1 -2
  37. package/dist/components/MessageInput/icons.d.ts +0 -1
  38. package/dist/components/MessageInput/icons.js +0 -3
  39. package/dist/components/MessageInput/types.d.ts +3 -30
  40. package/dist/components/MessageList/MessageList.d.ts +3 -1
  41. package/dist/components/MessageList/MessageList.js +2 -1
  42. package/dist/components/MessageList/VirtualizedMessageList.d.ts +3 -1
  43. package/dist/components/MessageList/VirtualizedMessageList.js +3 -3
  44. package/dist/components/MessageList/VirtualizedMessageListComponents.js +3 -2
  45. package/dist/components/MessageList/hooks/MessageList/useEnrichedMessages.d.ts +2 -1
  46. package/dist/components/MessageList/hooks/MessageList/useEnrichedMessages.js +3 -3
  47. package/dist/components/MessageList/utils.d.ts +1 -1
  48. package/dist/components/MessageList/utils.js +16 -6
  49. package/dist/components/ReactFileUtilities/types.d.ts +0 -29
  50. package/dist/components/ReactFileUtilities/utils.d.ts +2 -0
  51. package/dist/components/ReactFileUtilities/utils.js +2 -0
  52. package/dist/context/ChannelActionContext.d.ts +2 -2
  53. package/dist/context/MessageInputContext.d.ts +1 -5
  54. package/dist/i18n/Streami18n.d.ts +2 -0
  55. package/dist/i18n/de.json +3 -1
  56. package/dist/i18n/en.json +3 -1
  57. package/dist/i18n/es.json +3 -1
  58. package/dist/i18n/fr.json +3 -1
  59. package/dist/i18n/hi.json +3 -1
  60. package/dist/i18n/it.json +3 -1
  61. package/dist/i18n/ja.json +3 -1
  62. package/dist/i18n/ko.json +3 -1
  63. package/dist/i18n/nl.json +3 -1
  64. package/dist/i18n/pt.json +3 -1
  65. package/dist/i18n/ru.json +3 -1
  66. package/dist/i18n/tr.json +3 -1
  67. package/dist/i18n/utils.d.ts +3 -3
  68. package/dist/index.cjs.js +1987 -12143
  69. package/dist/index.cjs.js.map +4 -4
  70. package/dist/{components → plugins}/Emojis/EmojiPicker.js +1 -1
  71. package/dist/plugins/Emojis/icons.d.ts +2 -0
  72. package/dist/plugins/Emojis/icons.js +4 -0
  73. package/dist/{components → plugins}/Emojis/index.cjs.js +23 -22
  74. package/dist/plugins/Emojis/index.cjs.js.map +7 -0
  75. package/dist/plugins/Emojis/index.d.ts +2 -0
  76. package/dist/plugins/Emojis/index.js +2 -0
  77. package/dist/plugins/encoders/mp3.cjs.js +111 -0
  78. package/dist/plugins/encoders/mp3.cjs.js.map +7 -0
  79. package/dist/{components/MediaRecorder/transcode → plugins/encoders}/mp3.js +3 -3
  80. package/package.json +16 -6
  81. package/dist/components/Emojis/index.cjs.js.map +0 -7
  82. package/dist/components/Emojis/index.d.ts +0 -1
  83. package/dist/components/Emojis/index.js +0 -1
  84. package/dist/components/MessageInput/AttachmentPreviewList/UploadPreviewItem.d.ts +0 -11
  85. package/dist/components/MessageInput/AttachmentPreviewList/UploadPreviewItem.js +0 -51
  86. package/dist/components/MessageInput/hooks/useFileUploads.d.ts +0 -7
  87. package/dist/components/MessageInput/hooks/useFileUploads.js +0 -85
  88. package/dist/components/MessageInput/hooks/useImageUploads.d.ts +0 -8
  89. package/dist/components/MessageInput/hooks/useImageUploads.js +0 -94
  90. /package/dist/{components → plugins}/Emojis/EmojiPicker.d.ts +0 -0
  91. /package/dist/{components/MediaRecorder/transcode → plugins/encoders}/mp3.d.ts +0 -0
package/README.md CHANGED
@@ -110,3 +110,13 @@ We recently closed a [$38 million Series B funding round](https://techcrunch.com
110
110
  Our APIs are used by more than a billion end-users, and by working at Stream, you have the chance to make a huge impact on a team of very strong engineers.
111
111
 
112
112
  Check out our current openings and apply via [Stream's website](https://getstream.io/team/#jobs).
113
+
114
+ ## Acknowledgements
115
+
116
+ ### Lamejs
117
+
118
+ This project uses `lamejs` library under the LGPL license to convert the recorded audio to mp3 format.
119
+ The library source code is dynamically imported and used only if audio recording is enabled.
120
+
121
+ You can obtain the source code for `lamejs` from the [lamejs repository](https://github.com/gideonstele/lamejs) that is a fork of [the original JS library](https://github.com/zhuker/lamejs).
122
+ You can find the source code for LAME at https://lame.sourceforge.net and its license at: https://lame.sourceforge.net/license.txt
@@ -9,6 +9,8 @@ type WaveProgressBarProps = {
9
9
  amplitudesCount?: number;
10
10
  /** Progress expressed in fractional number value btw 0 and 100. */
11
11
  progress?: number;
12
+ relativeAmplitudeBarWidth?: number;
13
+ relativeAmplitudeGap?: number;
12
14
  };
13
- export declare const WaveProgressBar: ({ amplitudesCount, progress, seek, waveformData, }: WaveProgressBarProps) => React.JSX.Element | null;
15
+ export declare const WaveProgressBar: ({ amplitudesCount, progress, relativeAmplitudeBarWidth, relativeAmplitudeGap, seek, waveformData, }: WaveProgressBarProps) => React.JSX.Element | null;
14
16
  export {};
@@ -1,10 +1,13 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
1
+ import throttle from 'lodash.throttle';
2
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
2
3
  import clsx from 'clsx';
3
4
  import { resampleWaveformData } from '../audioSampling';
4
- export const WaveProgressBar = ({ amplitudesCount = 40, progress = 0, seek, waveformData, }) => {
5
+ export const WaveProgressBar = ({ amplitudesCount = 40, progress = 0, relativeAmplitudeBarWidth = 2, relativeAmplitudeGap = 1, seek, waveformData, }) => {
5
6
  const [progressIndicator, setProgressIndicator] = useState(null);
6
7
  const isDragging = useRef(false);
7
- const rootRef = useRef(null);
8
+ const [root, setRoot] = useState(null);
9
+ const [trackAxisX, setTrackAxisX] = useState();
10
+ const lastRootWidth = useRef();
8
11
  const handleDragStart = (e) => {
9
12
  e.preventDefault();
10
13
  if (!progressIndicator)
@@ -25,22 +28,54 @@ export const WaveProgressBar = ({ amplitudesCount = 40, progress = 0, seek, wave
25
28
  isDragging.current = false;
26
29
  progressIndicator.style.removeProperty('cursor');
27
30
  }, [progressIndicator]);
28
- const resampledWaveformData = useMemo(() => resampleWaveformData(waveformData, amplitudesCount), [
29
- amplitudesCount,
30
- waveformData,
31
- ]);
31
+ const getTrackAxisX = useMemo(() => throttle((rootWidth) => {
32
+ if (rootWidth === lastRootWidth.current)
33
+ return;
34
+ lastRootWidth.current = rootWidth;
35
+ const possibleAmpCount = Math.floor(rootWidth / (relativeAmplitudeGap + relativeAmplitudeBarWidth));
36
+ const tooManyAmplitudesToRender = possibleAmpCount < amplitudesCount;
37
+ const barCount = tooManyAmplitudesToRender ? possibleAmpCount : amplitudesCount;
38
+ const amplitudeBarWidthToGapRatio = relativeAmplitudeBarWidth / (relativeAmplitudeBarWidth + relativeAmplitudeGap);
39
+ const barWidth = barCount && (rootWidth / barCount) * amplitudeBarWidthToGapRatio;
40
+ setTrackAxisX({
41
+ barCount,
42
+ barWidth,
43
+ gap: barWidth * (relativeAmplitudeGap / relativeAmplitudeBarWidth),
44
+ });
45
+ }, 1), [relativeAmplitudeBarWidth, relativeAmplitudeGap, amplitudesCount]);
46
+ const resampledWaveformData = useMemo(() => (trackAxisX ? resampleWaveformData(waveformData, trackAxisX.barCount) : []), [trackAxisX, waveformData]);
32
47
  useEffect(() => {
33
48
  document.addEventListener('pointerup', handleDragStop);
34
49
  return () => {
35
50
  document.removeEventListener('pointerup', handleDragStop);
36
51
  };
37
52
  }, [handleDragStop]);
38
- if (!waveformData.length)
53
+ useEffect(() => {
54
+ if (!root || typeof ResizeObserver === 'undefined')
55
+ return;
56
+ const observer = new ResizeObserver(([entry]) => {
57
+ getTrackAxisX(entry.contentRect.width);
58
+ });
59
+ observer.observe(root);
60
+ return () => {
61
+ observer.disconnect();
62
+ };
63
+ }, [getTrackAxisX, root]);
64
+ useLayoutEffect(() => {
65
+ if (!root)
66
+ return;
67
+ const { width: rootWidth } = root.getBoundingClientRect();
68
+ getTrackAxisX(rootWidth);
69
+ }, [getTrackAxisX, root]);
70
+ if (!waveformData.length || trackAxisX?.barCount === 0)
39
71
  return null;
40
- return (React.createElement("div", { className: 'str-chat__wave-progress-bar__track', "data-testid": 'wave-progress-bar-track', onClick: seek, onPointerDown: handleDragStart, onPointerMove: handleDrag, onPointerUp: handleDragStop, ref: rootRef, role: 'progressbar' },
72
+ return (React.createElement("div", { className: 'str-chat__wave-progress-bar__track', "data-testid": 'wave-progress-bar-track', onClick: seek, onPointerDown: handleDragStart, onPointerMove: handleDrag, onPointerUp: handleDragStop, ref: setRoot, role: 'progressbar', style: {
73
+ '--str-chat__voice-recording-amplitude-bar-gap-width': trackAxisX?.gap + 'px',
74
+ } },
41
75
  resampledWaveformData.map((amplitude, i) => (React.createElement("div", { className: clsx('str-chat__wave-progress-bar__amplitude-bar', {
42
76
  ['str-chat__wave-progress-bar__amplitude-bar--active']: progress > (i / resampledWaveformData.length) * 100,
43
77
  }), "data-testid": 'amplitude-bar', key: `amplitude-${i}`, style: {
78
+ '--str-chat__voice-recording-amplitude-bar-width': trackAxisX?.barWidth + 'px',
44
79
  '--str-chat__wave-progress-bar__amplitude-bar-height': amplitude
45
80
  ? amplitude * 100 + '%'
46
81
  : '0%',
@@ -102,6 +102,7 @@ export const channelReducer = (state, action) => {
102
102
  return {
103
103
  ...state,
104
104
  thread: message,
105
+ threadHasMore: true,
105
106
  threadMessages: message.id ? { ...channel.state.threads }[message.id] || [] : [],
106
107
  threadSuppressAutoscroll: false,
107
108
  };
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { useTranslationContext } from '../../context/TranslationContext';
3
3
  import { getDateString } from '../../i18n/utils';
4
4
  const UnMemoizedDateSeparator = (props) => {
5
- const { calendar = true, date: messageCreatedAt, formatDate, position = 'right', unread, ...restTimestampFormatterOptions } = props;
5
+ const { calendar, date: messageCreatedAt, formatDate, position = 'right', unread, ...restTimestampFormatterOptions } = props;
6
6
  const { t, tDateTimeParser } = useTranslationContext('DateSeparator');
7
7
  const formattedDate = getDateString({
8
8
  calendar,
@@ -6,7 +6,7 @@ import { getDateString } from '../../i18n/utils';
6
6
  * Component to display system and channel event messages
7
7
  */
8
8
  const UnMemoizedEventComponent = (props) => {
9
- const { calendar, calendarFormats, format = 'dddd L', Avatar = DefaultAvatar, message } = props;
9
+ const { calendar, calendarFormats, format, Avatar = DefaultAvatar, message } = props;
10
10
  const { t, tDateTimeParser } = useTranslationContext('EventComponent');
11
11
  const { created_at = '', event, text, type } = message;
12
12
  const getDateOptions = { messageCreatedAt: created_at.toString(), tDateTimeParser };
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useLayoutEffect, useRef } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { deprecationAndReplacementWarning } from '../../utils/deprecationWarning';
3
3
  import { DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD } from '../../constants/limits';
4
4
  /**
@@ -17,6 +17,8 @@ export const InfiniteScroll = (props) => {
17
17
  const hasNextPageFlag = hasNextPage || hasMoreNewer;
18
18
  const hasPreviousPageFlag = hasPreviousPage || hasMore;
19
19
  const scrollComponent = useRef();
20
+ const previousOffset = useRef();
21
+ const previousReverseOffset = useRef();
20
22
  const scrollListenerRef = useRef();
21
23
  scrollListenerRef.current = () => {
22
24
  const element = scrollComponent.current;
@@ -32,7 +34,11 @@ export const InfiniteScroll = (props) => {
32
34
  }
33
35
  if (isLoading)
34
36
  return;
35
- // FIXME: this triggers loadMore call when a user types messages in thread and the scroll container container expands
37
+ if (previousOffset.current === offset && previousReverseOffset.current === reverseOffset)
38
+ return;
39
+ previousOffset.current = offset;
40
+ previousReverseOffset.current = reverseOffset;
41
+ // FIXME: this triggers loadMore call when a user types messages in thread and the scroll container expands
36
42
  if (reverseOffset < Number(threshold) &&
37
43
  typeof loadPreviousPageFn === 'function' &&
38
44
  hasPreviousPageFlag) {
@@ -51,7 +57,7 @@ export const InfiniteScroll = (props) => {
51
57
  ], 'InfiniteScroll');
52
58
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
59
  }, []);
54
- useLayoutEffect(() => {
60
+ useEffect(() => {
55
61
  const scrollElement = scrollComponent.current?.parentNode;
56
62
  if (!scrollElement)
57
63
  return;
@@ -1,26 +1,25 @@
1
1
  import { AmplitudeRecorder, AmplitudeRecorderConfig } from './AmplitudeRecorder';
2
2
  import { BrowserPermission } from './BrowserPermission';
3
3
  import { BehaviorSubject, Subject } from '../observable';
4
+ import { TranscoderConfig } from '../transcode';
4
5
  import { RecordedMediaType } from '../../ReactFileUtilities';
5
6
  import { TranslationContextValue } from '../../../context';
6
7
  import type { LocalVoiceRecordingAttachment } from '../../MessageInput';
7
8
  import type { DefaultStreamChatGenerics } from '../../../types';
8
- export declare const POSSIBLE_TRANSCODING_MIME_TYPES: readonly ["audio/wav", "audio/mp3"];
9
9
  export declare const DEFAULT_MEDIA_RECORDER_CONFIG: MediaRecorderConfig;
10
10
  export declare const DEFAULT_AUDIO_TRANSCODER_CONFIG: TranscoderConfig;
11
- type SupportedTranscodeMimeTypes = typeof POSSIBLE_TRANSCODING_MIME_TYPES[number];
12
- export type TranscoderConfig = {
13
- sampleRate: number;
14
- targetMimeType: SupportedTranscodeMimeTypes;
15
- };
16
11
  type MediaRecorderConfig = Omit<MediaRecorderOptions, 'mimeType'> & Required<Pick<MediaRecorderOptions, 'mimeType'>>;
17
12
  export type AudioRecorderConfig = {
18
13
  amplitudeRecorderConfig: AmplitudeRecorderConfig;
19
14
  mediaRecorderConfig: MediaRecorderOptions;
20
15
  transcoderConfig: TranscoderConfig;
21
16
  };
17
+ type PartialValues<T> = {
18
+ [P in keyof T]?: Partial<T[P]>;
19
+ };
20
+ export type CustomAudioRecordingConfig = PartialValues<AudioRecorderConfig>;
22
21
  export type AudioRecorderOptions = {
23
- config?: Partial<AudioRecorderConfig>;
22
+ config?: CustomAudioRecordingConfig;
24
23
  generateRecordingTitle?: (mimeType: string) => string;
25
24
  t?: TranslationContextValue['t'];
26
25
  };
@@ -15,7 +15,6 @@ const RECORDED_MIME_TYPE_BY_BROWSER = {
15
15
  safari: 'audio/mp4;codecs=mp4a.40.2',
16
16
  },
17
17
  };
18
- export const POSSIBLE_TRANSCODING_MIME_TYPES = ['audio/wav', 'audio/mp3'];
19
18
  export const DEFAULT_MEDIA_RECORDER_CONFIG = {
20
19
  mimeType: isSafari()
21
20
  ? RECORDED_MIME_TYPE_BY_BROWSER.audio.safari
@@ -23,7 +22,6 @@ export const DEFAULT_MEDIA_RECORDER_CONFIG = {
23
22
  };
24
23
  export const DEFAULT_AUDIO_TRANSCODER_CONFIG = {
25
24
  sampleRate: 16000,
26
- targetMimeType: 'audio/mp3',
27
25
  };
28
26
  const disposeOfMediaStream = (stream) => {
29
27
  if (!stream?.active)
@@ -245,9 +243,6 @@ export class MediaRecorderController {
245
243
  this.amplitudeRecorderConfig = mergeDeepUndefined({ ...config?.amplitudeRecorderConfig }, DEFAULT_AMPLITUDE_RECORDER_CONFIG);
246
244
  this.mediaRecorderConfig = mergeDeepUndefined({ ...config?.mediaRecorderConfig }, DEFAULT_MEDIA_RECORDER_CONFIG);
247
245
  this.transcoderConfig = mergeDeepUndefined({ ...config?.transcoderConfig }, DEFAULT_AUDIO_TRANSCODER_CONFIG);
248
- if (!POSSIBLE_TRANSCODING_MIME_TYPES.includes(this.transcoderConfig.targetMimeType)) {
249
- this.transcoderConfig.targetMimeType = DEFAULT_AUDIO_TRANSCODER_CONFIG.targetMimeType;
250
- }
251
246
  const mediaType = getRecordedMediaTypeFromMimeType(this.mediaRecorderConfig.mimeType);
252
247
  if (!mediaType) {
253
248
  throw new Error(`Unsupported media type (supported audio or video only). Provided mimeType: ${this.mediaRecorderConfig.mimeType}`);
@@ -1 +1 @@
1
- export type { CustomAudioRecordingConfig, RecordingController } from './useMediaRecorder';
1
+ export type { RecordingController } from './useMediaRecorder';
@@ -1,8 +1,7 @@
1
1
  import { MessageInputContextValue } from '../../../context';
2
- import { AudioRecorderConfig, MediaRecorderController, MediaRecordingState } from '../classes';
2
+ import { CustomAudioRecordingConfig, MediaRecorderController, MediaRecordingState } from '../classes';
3
3
  import type { LocalVoiceRecordingAttachment } from '../../MessageInput';
4
4
  import type { DefaultStreamChatGenerics } from '../../../types';
5
- export type CustomAudioRecordingConfig = Partial<AudioRecorderConfig>;
6
5
  export type RecordingController<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
7
6
  completeRecording: () => void;
8
7
  permissionState?: PermissionState;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useTranslationContext } from '../../../context';
3
- import { MediaRecorderController } from '../classes';
3
+ import { MediaRecorderController, } from '../classes';
4
4
  export const useMediaRecorder = ({ asyncMessagesMultiSendEnabled, enabled, generateRecordingTitle, handleSubmit, recordingConfig, uploadAttachment, }) => {
5
5
  const { t } = useTranslationContext('useMediaRecorder');
6
6
  const [recording, setRecording] = useState();
@@ -3,3 +3,4 @@ export * from './AudioRecorder';
3
3
  export * from './hooks';
4
4
  export { MediaRecordingState } from './classes/MediaRecorderController';
5
5
  export { RecordingPermission } from './classes/BrowserPermission';
6
+ export type { CustomAudioRecordingConfig } from './classes';
@@ -1,7 +1,8 @@
1
- type TranscodeParams = {
2
- blob: Blob;
1
+ export type TranscoderConfig = {
3
2
  sampleRate: number;
4
- targetMimeType: string;
3
+ encoder?: (file: File, sampleRate: number) => Promise<Blob>;
4
+ };
5
+ export type TranscodeParams = TranscoderConfig & {
6
+ blob: Blob;
5
7
  };
6
- export declare const transcode: ({ blob, sampleRate, targetMimeType }: TranscodeParams) => Promise<Blob>;
7
- export {};
8
+ export declare const transcode: ({ blob, encoder, sampleRate, }: TranscodeParams) => Promise<Blob>;
@@ -1,17 +1,7 @@
1
1
  import { encodeToWaw } from './wav';
2
- import { encodeToMp3 } from './mp3';
3
2
  import { createFileFromBlobs, getExtensionFromMimeType } from '../../ReactFileUtilities';
4
- export const transcode = ({ blob, sampleRate, targetMimeType }) => {
5
- const file = createFileFromBlobs({
6
- blobsArray: [blob],
7
- fileName: `audio_recording_${new Date().toISOString()}.${getExtensionFromMimeType(blob.type)}`,
8
- mimeType: blob.type,
9
- });
10
- if (targetMimeType.match('audio/wav')) {
11
- return encodeToWaw(file, sampleRate);
12
- }
13
- if (targetMimeType.match('audio/mp3')) {
14
- return encodeToMp3(file, sampleRate);
15
- }
16
- return Promise.resolve(blob);
17
- };
3
+ export const transcode = ({ blob, encoder = encodeToWaw, sampleRate, }) => encoder(createFileFromBlobs({
4
+ blobsArray: [blob],
5
+ fileName: `audio_recording_${new Date().toISOString()}.${getExtensionFromMimeType(blob.type)}`,
6
+ mimeType: blob.type,
7
+ }), sampleRate);
@@ -92,7 +92,7 @@ const MessageSimpleWithContext = (props) => {
92
92
  showMetadata && (React.createElement("div", { className: 'str-chat__message-metadata' },
93
93
  React.createElement(MessageStatus, null),
94
94
  !isMyMessage() && !!message.user && (React.createElement("span", { className: 'str-chat__message-simple-name' }, message.user.name || message.user.id)),
95
- React.createElement(MessageTimestamp, { calendar: true, customClass: 'str-chat__message-simple-timestamp' }),
95
+ React.createElement(MessageTimestamp, { customClass: 'str-chat__message-simple-timestamp' }),
96
96
  isEdited && (React.createElement("span", { className: 'str-chat__mesage-simple-edited' }, t('Edited'))),
97
97
  isEdited && React.createElement(MessageEditedTimestamp, { calendar: true, open: isEditedTimestampOpen }))))));
98
98
  };
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import type { StreamMessage } from '../../context/ChannelStateContext';
3
3
  import type { DefaultStreamChatGenerics } from '../../types/types';
4
4
  import type { TimestampFormatterOptions } from '../../i18n/utils';
5
- export declare const defaultTimestampFormat = "h:mmA";
6
5
  export type MessageTimestampProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = TimestampFormatterOptions & {
7
6
  customClass?: string;
8
7
  message?: StreamMessage<StreamChatGenerics>;
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import { useMessageContext } from '../../context/MessageContext';
3
3
  import { Timestamp as DefaultTimestamp } from './Timestamp';
4
4
  import { useComponentContext } from '../../context';
5
- export const defaultTimestampFormat = 'h:mmA';
6
5
  const UnMemoizedMessageTimestamp = (props) => {
7
6
  const { message: propMessage, ...timestampProps } = props;
8
7
  const { message: contextMessage } = useMessageContext('MessageTimestamp');
@@ -4,5 +4,4 @@ export interface TimestampProps extends TimestampFormatterOptions {
4
4
  customClass?: string;
5
5
  timestamp?: Date | string;
6
6
  }
7
- export declare const defaultTimestampFormat = "h:mmA";
8
7
  export declare function Timestamp(props: TimestampProps): React.JSX.Element | null;
@@ -2,9 +2,8 @@ import React, { useMemo } from 'react';
2
2
  import { useMessageContext } from '../../context/MessageContext';
3
3
  import { isDate, useTranslationContext } from '../../context/TranslationContext';
4
4
  import { getDateString } from '../../i18n/utils';
5
- export const defaultTimestampFormat = 'h:mmA';
6
5
  export function Timestamp(props) {
7
- const { calendar, calendarFormats, customClass, format = defaultTimestampFormat, timestamp, } = props;
6
+ const { calendar, calendarFormats, customClass, format, timestamp } = props;
8
7
  const { formatDate } = useMessageContext('MessageTimestamp');
9
8
  const { t, tDateTimeParser } = useTranslationContext('MessageTimestamp');
10
9
  const normalizedTimestamp = timestamp && isDate(timestamp) ? timestamp.toISOString() : timestamp;
@@ -16,7 +15,7 @@ export function Timestamp(props) {
16
15
  messageCreatedAt: normalizedTimestamp,
17
16
  t,
18
17
  tDateTimeParser,
19
- timestampTranslationKey: 'timestamp/Timestamp',
18
+ timestampTranslationKey: 'timestamp/MessageTimestamp',
20
19
  }), [calendar, calendarFormats, format, formatDate, normalizedTimestamp, t, tDateTimeParser]);
21
20
  if (!when) {
22
21
  return null;
@@ -11,7 +11,7 @@ const visitor = (node, index, parent) => {
11
11
  if (!prevSibling?.position)
12
12
  return;
13
13
  if (node.position.start.line === prevSibling.position.start.line)
14
- return false;
14
+ return;
15
15
  const ownStartLine = node.position.start.line;
16
16
  const prevEndLine = prevSibling.position.end.line;
17
17
  // the -1 is adjustment for the single line break into which multiple line breaks are converted
@@ -212,6 +212,8 @@ export const areMessageUIPropsEqual = (prevProps, nextProps) => {
212
212
  return false;
213
213
  if (prevProps.readBy?.length !== nextProps.readBy?.length)
214
214
  return false;
215
+ if (prevProps.groupStyles !== nextProps.groupStyles)
216
+ return false;
215
217
  if (prevProps.showDetailedReactions !== nextProps.showDetailedReactions) {
216
218
  return false;
217
219
  }
@@ -2,37 +2,33 @@ import React from 'react';
2
2
  import { UnsupportedAttachmentPreview as DefaultUnknownAttachmentPreview, } from './UnsupportedAttachmentPreview';
3
3
  import { VoiceRecordingPreview as DefaultVoiceRecordingPreview, } from './VoiceRecordingPreview';
4
4
  import { FileAttachmentPreview as DefaultFilePreview, } from './FileAttachmentPreview';
5
- import { FileUploadPreviewAdapter, ImageUploadPreviewAdapter } from './UploadPreviewItem';
6
5
  import { ImageAttachmentPreview as DefaultImagePreview, } from './ImageAttachmentPreview';
7
6
  import { isLocalAttachment, isLocalAudioAttachment, isLocalFileAttachment, isLocalImageAttachment, isLocalMediaAttachment, isLocalVoiceRecordingAttachment, isScrapedContent, } from '../../Attachment';
8
7
  import { useMessageInputContext } from '../../../context';
9
8
  export const AttachmentPreviewList = ({ AudioAttachmentPreview = DefaultFilePreview, FileAttachmentPreview = DefaultFilePreview, ImageAttachmentPreview = DefaultImagePreview, UnsupportedAttachmentPreview = DefaultUnknownAttachmentPreview, VideoAttachmentPreview = DefaultFilePreview, VoiceRecordingPreview = DefaultVoiceRecordingPreview, }) => {
10
- const { attachments, fileOrder, imageOrder, removeAttachments, uploadAttachment, } = useMessageInputContext('AttachmentPreviewList');
9
+ const { attachments, removeAttachments, uploadAttachment, } = useMessageInputContext('AttachmentPreviewList');
11
10
  return (React.createElement("div", { className: 'str-chat__attachment-preview-list' },
12
- React.createElement("div", { className: 'str-chat__attachment-list-scroll-container', "data-testid": 'attachment-list-scroll-container' },
13
- attachments.map((attachment) => {
14
- if (isScrapedContent(attachment))
15
- return null;
16
- if (isLocalVoiceRecordingAttachment(attachment)) {
17
- return (React.createElement(VoiceRecordingPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
18
- }
19
- else if (isLocalAudioAttachment(attachment)) {
20
- return (React.createElement(AudioAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
21
- }
22
- else if (isLocalMediaAttachment(attachment)) {
23
- return (React.createElement(VideoAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
24
- }
25
- else if (isLocalImageAttachment(attachment)) {
26
- return (React.createElement(ImageAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.image_url, removeAttachments: removeAttachments }));
27
- }
28
- else if (isLocalFileAttachment(attachment)) {
29
- return (React.createElement(FileAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
30
- }
31
- else if (isLocalAttachment(attachment)) {
32
- return (React.createElement(UnsupportedAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id, removeAttachments: removeAttachments }));
33
- }
11
+ React.createElement("div", { className: 'str-chat__attachment-list-scroll-container', "data-testid": 'attachment-list-scroll-container' }, attachments.map((attachment) => {
12
+ if (isScrapedContent(attachment))
34
13
  return null;
35
- }),
36
- imageOrder.map((id) => (React.createElement(ImageUploadPreviewAdapter, { id: id, key: id, Preview: ImageAttachmentPreview }))),
37
- fileOrder.map((id) => (React.createElement(FileUploadPreviewAdapter, { id: id, key: id, Preview: FileAttachmentPreview }))))));
14
+ if (isLocalVoiceRecordingAttachment(attachment)) {
15
+ return (React.createElement(VoiceRecordingPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
16
+ }
17
+ else if (isLocalAudioAttachment(attachment)) {
18
+ return (React.createElement(AudioAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
19
+ }
20
+ else if (isLocalMediaAttachment(attachment)) {
21
+ return (React.createElement(VideoAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
22
+ }
23
+ else if (isLocalImageAttachment(attachment)) {
24
+ return (React.createElement(ImageAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.image_url, removeAttachments: removeAttachments }));
25
+ }
26
+ else if (isLocalFileAttachment(attachment)) {
27
+ return (React.createElement(FileAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id || attachment.asset_url, removeAttachments: removeAttachments }));
28
+ }
29
+ else if (isLocalAttachment(attachment)) {
30
+ return (React.createElement(UnsupportedAttachmentPreview, { attachment: attachment, handleRetry: uploadAttachment, key: attachment.localMetadata.id, removeAttachments: removeAttachments }));
31
+ }
32
+ return null;
33
+ }))));
38
34
  };
@@ -4,6 +4,7 @@ import { LocalAttachmentCast, LocalAttachmentUploadMetadata } from '../types';
4
4
  import type { DefaultStreamChatGenerics } from '../../../types';
5
5
  type FileLikeAttachment = {
6
6
  asset_url?: string;
7
+ file_size?: number;
7
8
  mime_type?: string;
8
9
  title?: string;
9
10
  };
@@ -13,7 +13,7 @@ export const FileAttachmentPreview = ({ attachment, handleRetry, removeAttachmen
13
13
  React.createElement(RetryIcon, null))),
14
14
  React.createElement("div", { className: 'str-chat__attachment-preview-file-end' },
15
15
  React.createElement("div", { className: 'str-chat__attachment-preview-file-name', title: attachment.title }, attachment.title),
16
- attachment.localMetadata?.uploadState === 'finished' && !!attachment.asset_url && (React.createElement("a", { className: 'str-chat__attachment-preview-file-download', download: true, href: attachment.asset_url, rel: 'noreferrer', target: '_blank' },
16
+ attachment.localMetadata?.uploadState === 'finished' && !!attachment.asset_url && (React.createElement("a", { "aria-label": t('aria/Download attachment'), className: 'str-chat__attachment-preview-file-download', download: true, href: attachment.asset_url, rel: 'noreferrer', target: '_blank', title: t('Download attachment {{ name }}', { name: attachment.title }) },
17
17
  React.createElement(DownloadIcon, null))),
18
18
  attachment.localMetadata?.uploadState === 'uploading' && (React.createElement(LoadingIndicatorIcon, { size: 17 })))));
19
19
  };
@@ -9,6 +9,7 @@ export const ImageAttachmentPreview = ({ attachment, handleRetry, removeAttachme
9
9
  const [previewError, setPreviewError] = useState(false);
10
10
  const { id, uploadState } = attachment.localMetadata ?? {};
11
11
  const handleLoadError = useCallback(() => setPreviewError(true), []);
12
+ const assetUrl = attachment.image_url || attachment.localMetadata.previewUri;
12
13
  return (React.createElement("div", { className: clsx('str-chat__attachment-preview-image', {
13
14
  'str-chat__attachment-preview-image--error': previewError,
14
15
  }), "data-testid": 'attachment-preview-image' },
@@ -18,5 +19,5 @@ export const ImageAttachmentPreview = ({ attachment, handleRetry, removeAttachme
18
19
  React.createElement(RetryIcon, null))),
19
20
  uploadState === 'uploading' && (React.createElement("div", { className: 'str-chat__attachment-preview-image-loading' },
20
21
  React.createElement(LoadingIndicatorIcon, { size: 17 }))),
21
- attachment.image_url && (React.createElement(BaseImage, { alt: attachment.title, className: 'str-chat__attachment-preview-thumbnail', onError: handleLoadError, src: attachment.image_url, title: attachment.title }))));
22
+ assetUrl && (React.createElement(BaseImage, { alt: attachment.fallback, className: 'str-chat__attachment-preview-thumbnail', onError: handleLoadError, src: assetUrl, title: attachment.fallback }))));
22
23
  };
@@ -2,11 +2,11 @@ import React from 'react';
2
2
  import { StreamMessage } from '../../context/ChannelStateContext';
3
3
  import { ComponentContextValue } from '../../context/ComponentContext';
4
4
  import type { Channel, Message, SendFileAPIResponse } from 'stream-chat';
5
+ import type { BaseLocalAttachmentMetadata, LocalAttachmentUploadMetadata } from './types';
5
6
  import type { SearchQueryParams } from '../ChannelSearch/hooks/useChannelSearch';
6
7
  import type { MessageToSend } from '../../context/ChannelActionContext';
7
8
  import type { CustomTrigger, DefaultStreamChatGenerics, SendMessageOptions, UnknownType } from '../../types/types';
8
9
  import type { URLEnrichmentConfig } from './hooks/useLinkPreviews';
9
- import type { FileUpload, ImageUpload } from './types';
10
10
  import type { CustomAudioRecordingConfig } from '../MediaRecorder';
11
11
  export type EmojiSearchIndexResult = {
12
12
  id: string;
@@ -39,15 +39,13 @@ export type MessageInputProps<StreamChatGenerics extends DefaultStreamChatGeneri
39
39
  /** If true, the suggestion list will not display and autocomplete @mentions. Default: false. */
40
40
  disableMentions?: boolean;
41
41
  /** Function to override the default file upload request */
42
- doFileUploadRequest?: (file: FileUpload['file'], channel: Channel<StreamChatGenerics>) => Promise<SendFileAPIResponse>;
42
+ doFileUploadRequest?: (file: LocalAttachmentUploadMetadata['file'], channel: Channel<StreamChatGenerics>) => Promise<SendFileAPIResponse>;
43
43
  /** Function to override the default image upload request */
44
- doImageUploadRequest?: (file: ImageUpload['file'], channel: Channel<StreamChatGenerics>) => Promise<SendFileAPIResponse>;
44
+ doImageUploadRequest?: (file: LocalAttachmentUploadMetadata['file'], channel: Channel<StreamChatGenerics>) => Promise<SendFileAPIResponse>;
45
45
  /** Mechanism to be used with autocomplete and text replace features of the `MessageInput` component, see [emoji-mart `SearchIndex`](https://github.com/missive/emoji-mart#%EF%B8%8F%EF%B8%8F-headless-search) */
46
46
  emojiSearchIndex?: ComponentContextValue['emojiSearchIndex'];
47
47
  /** Custom error handler function to be called with a file/image upload fails */
48
- errorHandler?: (error: Error, type: string, file: (FileUpload | ImageUpload)['file'] & {
49
- id?: string;
50
- }) => void;
48
+ errorHandler?: (error: Error, type: string, file: LocalAttachmentUploadMetadata['file'] & BaseLocalAttachmentMetadata) => void;
51
49
  /** If true, focuses the text input on component mount */
52
50
  focus?: boolean;
53
51
  /** Generates the default value for the underlying textarea element. The function's return value takes precedence before additionalTextareaProps.defaultValue. */
@@ -20,7 +20,7 @@ import { useMessageInputContext } from '../../context/MessageInputContext';
20
20
  import { useComponentContext } from '../../context/ComponentContext';
21
21
  export const MessageInputFlat = () => {
22
22
  const { t } = useTranslationContext('MessageInputFlat');
23
- const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, fileUploads, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, imageUploads, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
23
+ const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
24
24
  const { AudioRecorder = DefaultAudioRecorder, AttachmentPreviewList = DefaultAttachmentPreviewList, CooldownTimer = DefaultCooldownTimer, FileUploadIcon = DefaultUploadIcon, LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, RecordingPermissionDeniedNotification = DefaultRecordingPermissionDeniedNotification, SendButton = DefaultSendButton, StartRecordingAudioButton = DefaultStartRecordingAudioButton, EmojiPicker, } = useComponentContext('MessageInputFlat');
25
25
  const { acceptedFiles = [], multipleUploads, quotedMessage, } = useChannelStateContext('MessageInputFlat');
26
26
  const { setQuotedMessage } = useChannelActionContext('MessageInputFlat');
@@ -30,9 +30,7 @@ export const MessageInputFlat = () => {
30
30
  setShowRecordingPermissionDeniedNotification(false);
31
31
  }, []);
32
32
  const id = useMemo(() => nanoid(), []);
33
- const failedAttachmentsCount = useMemo(() => attachments.filter((a) => a.localMetadata?.uploadState === 'failed').length, [attachments]);
34
- const failedUploadsCount = useMemo(() => Object.values(fileUploads).filter((upload) => upload.state === 'failed').length +
35
- Object.values(imageUploads).filter((upload) => upload.state === 'failed').length, [fileUploads, imageUploads]);
33
+ const failedUploadsCount = useMemo(() => attachments.filter((a) => a.localMetadata?.uploadState === 'failed').length, [attachments]);
36
34
  const accept = useMemo(() => acceptedFiles.reduce((mediaTypeMap, mediaType) => {
37
35
  mediaTypeMap[mediaType] ?? (mediaTypeMap[mediaType] = []);
38
36
  return mediaTypeMap;
@@ -89,15 +87,14 @@ export const MessageInputFlat = () => {
89
87
  React.createElement("div", { className: 'str-chat__message-textarea-container' },
90
88
  displayQuotedMessage && React.createElement(QuotedMessagePreview, { quotedMessage: quotedMessage }),
91
89
  isUploadEnabled &&
92
- !!(numberOfUploads + failedUploadsCount ||
93
- (attachments.length && attachments.length !== linkPreviews.size)) && React.createElement(AttachmentPreviewList, null),
90
+ !!(numberOfUploads + failedUploadsCount || attachments.length > 0) && (React.createElement(AttachmentPreviewList, null)),
94
91
  React.createElement("div", { className: 'str-chat__message-textarea-with-emoji-picker' },
95
92
  React.createElement(ChatAutoComplete, null),
96
93
  EmojiPicker && React.createElement(EmojiPicker, null))),
97
94
  !hideSendButton && (React.createElement(React.Fragment, null, cooldownRemaining ? (React.createElement(CooldownTimer, { cooldownInterval: cooldownRemaining, setCooldownRemaining: setCooldownRemaining })) : (React.createElement(React.Fragment, null,
98
95
  React.createElement(SendButton, { disabled: !numberOfUploads &&
99
96
  !text.length &&
100
- attachments.length - failedAttachmentsCount === 0, sendMessage: handleSubmit }),
97
+ attachments.length - failedUploadsCount === 0, sendMessage: handleSubmit }),
101
98
  recordingEnabled && (React.createElement(StartRecordingAudioButton, { disabled: isRecording ||
102
99
  (!asyncMessagesMultiSendEnabled &&
103
100
  attachments.some((a) => a.type === RecordingAttachmentType.VOICE_RECORDING)), onClick: () => {
@@ -1,18 +1,14 @@
1
+ import type { FileLike } from '../../ReactFileUtilities';
1
2
  import type { Attachment } from 'stream-chat';
2
3
  import type { MessageInputReducerAction, MessageInputState } from './useMessageInputState';
3
4
  import type { MessageInputProps } from '../MessageInput';
4
5
  import type { LocalAttachment } from '../types';
5
- import type { FileLike } from '../../ReactFileUtilities';
6
6
  import type { CustomTrigger, DefaultStreamChatGenerics } from '../../../types/types';
7
7
  export declare const useAttachments: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, V extends CustomTrigger = CustomTrigger>(props: MessageInputProps<StreamChatGenerics, V>, state: MessageInputState<StreamChatGenerics>, dispatch: React.Dispatch<MessageInputReducerAction<StreamChatGenerics>>, textareaRef: React.MutableRefObject<HTMLTextAreaElement | undefined>) => {
8
8
  maxFilesLeft: number;
9
9
  numberOfUploads: number;
10
10
  removeAttachments: (ids: string[]) => void;
11
- removeFile: (id: string) => void;
12
- removeImage: (id: string) => void;
13
11
  uploadAttachment: (att: LocalAttachment<StreamChatGenerics>) => Promise<LocalAttachment<StreamChatGenerics> | undefined>;
14
- uploadFile: (id: string) => void;
15
- uploadImage: (id: string) => Promise<void>;
16
12
  uploadNewFiles: (files: FileList | File[] | FileLike[]) => void;
17
13
  upsertAttachments: (attachments: (Attachment<StreamChatGenerics> | LocalAttachment<StreamChatGenerics>)[]) => void;
18
14
  };