From fe6fc8f0f2d2e5b05c13a2de07599f32405e9a63 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Fri, 3 Jul 2026 18:01:29 +0200 Subject: [PATCH 1/5] First pass reviewed --- .../AIStateIndicator/hooks/useAIState.ts | 4 +- src/components/Attachment/Audio.tsx | 9 ++- src/components/Attachment/FileAttachment.tsx | 5 +- .../Attachment/LinkPreview/CardAudio.tsx | 10 +-- src/components/Attachment/VoiceRecording.tsx | 16 ++--- src/components/Badge/MediaBadge.tsx | 5 +- src/components/Channel/Channel.tsx | 22 ++++--- .../hooks/useCreateChannelStateContext.ts | 2 +- src/components/ChannelList/ChannelList.tsx | 23 ++++--- src/components/ChannelList/ChannelListUI.tsx | 4 +- .../ChannelList/hooks/useChannelListShape.ts | 62 +++++++++++-------- .../ChannelList/hooks/usePaginatedChannels.ts | 28 +++------ src/components/ChannelList/utils.ts | 33 +++------- .../ChannelListItem/ChannelListItem.tsx | 46 ++++++++------ .../ChannelListItem/hooks/useIsUserMuted.ts | 2 +- .../hooks/useMessageDeliveryStatus.ts | 25 +++++--- .../Chat/hooks/useChannelsQueryState.ts | 8 +-- .../Chat/hooks/useCreateChatClient.ts | 13 +--- ...eReportLostConnectionSystemNotification.ts | 7 ++- .../AudioRecorder/AudioRecorder.tsx | 6 +- .../classes/MediaRecorderController.ts | 16 ++--- .../MessageAlsoSentInChannelIndicator.tsx | 10 ++- .../Message/hooks/useReactionHandler.ts | 16 +++-- src/components/Message/hooks/useUserRole.ts | 14 ++--- .../MessageActions.defaults.tsx | 4 +- .../MessageActions/RemindMeSubmenu.tsx | 4 +- .../AudioAttachmentPreview.tsx | 10 +-- .../FileAttachmentPreview.tsx | 2 +- .../MessageComposer/hooks/useSubmitHandler.ts | 8 ++- .../ScrollToLatestMessageButton.tsx | 8 +-- .../VirtualizedMessageList/useGiphyPreview.ts | 10 +-- .../MessageList/hooks/useMarkRead.ts | 13 ++-- .../PollActions/SuggestPollOptionPrompt.tsx | 3 +- src/components/Poll/PollHeader.tsx | 2 +- src/components/Poll/PollOptionSelector.tsx | 4 +- src/components/Poll/PollVote.tsx | 4 +- .../Poll/hooks/useManagePollVotesRealtime.ts | 6 +- .../Reactions/MessageReactionsDetail.tsx | 2 +- .../hooks/useLatestMessagePreview.ts | 10 +-- src/context/ChatContext.tsx | 9 +-- .../ChannelFilesView/ChannelFilesView.tsx | 9 +-- .../ChannelManagementActions.defaults.tsx | 4 +- .../ChannelManagementView.tsx | 2 +- .../ChannelMediaView.utils.ts | 4 +- .../ChannelMemberActions.defaults.tsx | 6 +- .../ChannelMembersAddView.tsx | 2 +- .../ChannelMembersBrowseView.tsx | 2 +- .../ChannelMembersView.utils.ts | 6 +- src/utils/getChannel.ts | 8 ++- 49 files changed, 282 insertions(+), 246 deletions(-) diff --git a/src/components/AIStateIndicator/hooks/useAIState.ts b/src/components/AIStateIndicator/hooks/useAIState.ts index a3de43a63a..00ead401d5 100644 --- a/src/components/AIStateIndicator/hooks/useAIState.ts +++ b/src/components/AIStateIndicator/hooks/useAIState.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import type { AIState, Channel, Event } from 'stream-chat'; +import type { AIState, Channel } from 'stream-chat'; export const AIStates = { Error: 'AI_STATE_ERROR', @@ -23,7 +23,7 @@ export const useAIState = (channel?: Channel): { aiState: AIState } => { return; } - const indicatorChangedListener = channel.on('ai_indicator.update', (event: Event) => { + const indicatorChangedListener = channel.on('ai_indicator.update', (event) => { const { cid } = event; const state = event.ai_state as AIState; if (channel.cid === cid) { diff --git a/src/components/Attachment/Audio.tsx b/src/components/Attachment/Audio.tsx index 5ece01381f..8978516e6e 100644 --- a/src/components/Attachment/Audio.tsx +++ b/src/components/Attachment/Audio.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type { Attachment } from 'stream-chat'; +import type { Attachment, VoiceRecordingAttachment } from 'stream-chat'; import { FileSizeIndicator as DefaultFileSizeIndicator, @@ -87,9 +87,8 @@ const audioPlayerStateSelector = (state: AudioPlayerState) => ({ * Audio attachment with play/pause button and progress bar */ export const Audio = (props: AudioProps) => { - const { - attachment: { asset_url, duration, file_size, mime_type, title }, - } = props; + const { asset_url, custom, title } = props.attachment as VoiceRecordingAttachment; + const { duration, file_size, mime_type, waveform_data } = custom; /** * Introducing message context. This could be breaking change, therefore the fallback to {} is provided. @@ -111,7 +110,7 @@ export const Audio = (props: AudioProps) => { `${threadList ? (message.parent_id ?? message.id) : ''}${message.id}`, src: asset_url, title, - waveformData: props.attachment.waveform_data, + waveformData: waveform_data, }); return audioPlayer ? : null; diff --git a/src/components/Attachment/FileAttachment.tsx b/src/components/Attachment/FileAttachment.tsx index f52ed0330f..4581d55b39 100644 --- a/src/components/Attachment/FileAttachment.tsx +++ b/src/components/Attachment/FileAttachment.tsx @@ -16,6 +16,7 @@ export const FileAttachment = ({ attachment }: FileAttachmentProps) => { const { AttachmentFileIcon, FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); const FileIconComponent = AttachmentFileIcon ?? FileIcon; + const { file_size, mime_type } = attachment.custom; return (
{
@@ -36,7 +37,7 @@ export const FileAttachment = ({ attachment }: FileAttachmentProps) => {
- +
{ ); }; -export const CardAudio = ({ - attachment: { +export const CardAudio = ({ attachment }: AudioProps) => { + const { asset_url, author_name, - mime_type, + custom, og_scrape_url, text, title, title_link, - }, -}: AudioProps) => { + } = attachment; + const { mime_type } = custom; const url = title_link || og_scrape_url; const dataTestId = 'card-audio-widget'; const rootClassName = 'str-chat__message-attachment-card-audio-widget'; diff --git a/src/components/Attachment/VoiceRecording.tsx b/src/components/Attachment/VoiceRecording.tsx index d885023b83..b28ab6da48 100644 --- a/src/components/Attachment/VoiceRecording.tsx +++ b/src/components/Attachment/VoiceRecording.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type { Attachment } from 'stream-chat'; +import type { Attachment, VoiceRecordingAttachment } from 'stream-chat'; import { FileSizeIndicator as DefaultFileSizeIndicator } from './components'; import { FileIcon } from '../FileIcon'; @@ -103,14 +103,13 @@ export const VoiceRecordingPlayer = ({ playbackRates, }: VoiceRecordingPlayerProps) => { const { t } = useTranslationContext(); + const { asset_url, title = t('Voice message') } = attachment; const { - asset_url, duration = 0, file_size, mime_type, - title = t('Voice message'), waveform_data, - } = attachment; + } = (attachment as VoiceRecordingAttachment).custom; /** * Introducing message context. This could be breaking change, therefore the fallback to {} is provided. @@ -143,27 +142,28 @@ export type QuotedVoiceRecordingProps = Pick; export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) => { const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); + const { duration, file_size, mime_type } = (attachment as VoiceRecordingAttachment).custom; return (
- {attachment.duration ? ( + {duration ? ( ) : ( )}
- +
); }; diff --git a/src/components/Badge/MediaBadge.tsx b/src/components/Badge/MediaBadge.tsx index 05129ccbdc..2ec71d0dd6 100644 --- a/src/components/Badge/MediaBadge.tsx +++ b/src/components/Badge/MediaBadge.tsx @@ -1,6 +1,6 @@ import { IconMicrophoneSolid, IconVideoFill } from '../Icons'; import React, { type ComponentType } from 'react'; -import type { LocalAttachment } from 'stream-chat'; +import type { LocalAttachment, LocalVoiceRecordingAttachment } from 'stream-chat'; import clsx from 'clsx'; export type MediaBadgeVariant = 'video' | 'voice-recording' | string; @@ -17,6 +17,7 @@ const MediaBadgeVariantToIcon: Record = { export const MediaBadge = ({ attachment, variant }: MediaBadgeProps) => { const Icon = MediaBadgeVariantToIcon[variant]; + const { duration } = (attachment as LocalVoiceRecordingAttachment).custom; return (
{ })} > {Icon && } - {attachment.duration ?
{attachment.duration}
: null} + {duration ?
{duration}
: null}
); }; diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index e070a39eaf..1c5e226508 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -350,10 +350,11 @@ const ChannelInner = ( ); const handleEvent = async (event: Event) => { - if (event.message) { + const message = (event as Extract).message; + if (message) { dispatch({ channel, - message: event.message, + message, type: 'updateThreadOnEvent', }); } @@ -761,7 +762,7 @@ const ChannelInner = ( await channel.query( { messages: { - created_at_around: channelUnreadUiState.last_read.toISOString(), + created_at_around: channelUnreadUiState.last_read, limit: queryMessageLimit, }, }, @@ -786,9 +787,8 @@ const ChannelInner = ( ); return; } - const firstMessageTimestamp = new Date( - firstMessageWithCreationDate.created_at as string, - ).getTime(); + const firstMessageTimestamp = + firstMessageWithCreationDate.created_at.getTime(); if (lastReadTimestamp < firstMessageTimestamp) { // whole channel is unread firstUnreadMessageId = firstMessageWithCreationDate.id; @@ -876,7 +876,10 @@ const ChannelInner = ( if (doDeleteMessageRequest) { deletedMessage = await doDeleteMessageRequest(message, options); } else { - const result = await client.deleteMessage(message.id, options); + const result = await client.deleteMessage({ + id: message.id, + ...options, + }); deletedMessage = result.message; } @@ -911,7 +914,7 @@ const ChannelInner = ( if (doSendMessageRequest) { messageResponse = await doSendMessageRequest(channel, message, options); } else { - messageResponse = await channel.sendMessage(message, options); + messageResponse = await channel.sendMessage({ message, ...options }); } let existingMessage: LocalMessage | undefined = undefined; @@ -1075,9 +1078,10 @@ const ChannelInner = ( const oldestMessageId = oldMessages[0]?.id; try { - const queryResponse = await channel.getReplies(parentId, { + const queryResponse = await channel.getReplies({ id_lt: oldestMessageId, limit, + parent_id: parentId, }); const threadHasMoreMessages = hasMoreMessagesProbably( diff --git a/src/components/Channel/hooks/useCreateChannelStateContext.ts b/src/components/Channel/hooks/useCreateChannelStateContext.ts index 07232a14c2..60a3b998a7 100644 --- a/src/components/Channel/hooks/useCreateChannelStateContext.ts +++ b/src/components/Channel/hooks/useCreateChannelStateContext.ts @@ -140,7 +140,7 @@ export const useCreateChannelStateContext = ( }), // eslint-disable-next-line react-hooks/exhaustive-deps [ - channel.data?.name, // otherwise ChannelHeader will not be updated + channel.data?.custom?.name, channelId, channelUnreadUiState, error, diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index b3e8153db7..fcb46b7954 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -7,6 +7,7 @@ import type { ChannelOptions, ChannelSort, Event, + EventPayload, SearchControllerState, } from 'stream-chat'; @@ -44,7 +45,7 @@ import { useStableId } from '../UtilityComponents/useStableId'; const DEFAULT_FILTERS = {}; const DEFAULT_OPTIONS = {}; -const DEFAULT_SORT = {}; +const DEFAULT_SORT: ChannelSort = []; const searchControllerStateSelector = (nextValue: SearchControllerState) => ({ searchIsActive: nextValue.isActive, @@ -227,9 +228,12 @@ const UnMemoizedChannelList = (props: ChannelListProps) => { ); if (!customActiveChannelObject) { - [customActiveChannelObject] = await client.queryChannels({ - id: customActiveChannel, - }); + [customActiveChannelObject] = await client.queryChannelsAndHydrate( + { + filter_conditions: { id: customActiveChannel }, + }, + { withResponse: false }, + ); } if (customActiveChannelObject) { @@ -307,18 +311,19 @@ const UnMemoizedChannelList = (props: ChannelListProps) => { useConnectionRecoveredListener(forceUpdate); useEffect(() => { - const handleEvent = (event: Event) => { + const handleEvent = (event: EventPayload<'channel.deleted' | 'channel.hidden'>) => { if (event.cid === channel?.cid) { setActiveChannel(); } }; - client.on('channel.deleted', handleEvent); - client.on('channel.hidden', handleEvent); + const subscriptions = [ + client.on('channel.deleted', handleEvent), + client.on('channel.hidden', handleEvent), + ]; return () => { - client.off('channel.deleted', handleEvent); - client.off('channel.hidden', handleEvent); + subscriptions.forEach((subscription) => subscription.unsubscribe()); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [channel?.cid]); diff --git a/src/components/ChannelList/ChannelListUI.tsx b/src/components/ChannelList/ChannelListUI.tsx index cfc630da34..5859c2a86d 100644 --- a/src/components/ChannelList/ChannelListUI.tsx +++ b/src/components/ChannelList/ChannelListUI.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; -import type { APIErrorResponse, Channel, ErrorFromResponse } from 'stream-chat'; +import type { APIErrorResponse, Channel, StreamAPIError } from 'stream-chat'; import { LoadingChannels } from '../Loading/LoadingChannels'; import { NullComponent } from '../UtilityComponents'; @@ -8,7 +8,7 @@ import { useComponentContext, useTranslationContext } from '../../context'; export type ChannelListUIProps = { /** Whether the channel query request returned an errored response */ - error: ErrorFromResponse | null; + error: StreamAPIError | null; /** The channels currently loaded in the list, only defined if `sendChannelsToList` on `ChannelList` is true */ loadedChannels?: Channel[]; /** Whether the channels are currently loading */ diff --git a/src/components/ChannelList/hooks/useChannelListShape.ts b/src/components/ChannelList/hooks/useChannelListShape.ts index fa5feb7f94..14f279ebc6 100644 --- a/src/components/ChannelList/hooks/useChannelListShape.ts +++ b/src/components/ChannelList/hooks/useChannelListShape.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import type { Channel, Event } from 'stream-chat'; +import type { Channel, Event, EventPayload } from 'stream-chat'; import type { Dispatch, SetStateAction } from 'react'; import { @@ -17,61 +17,73 @@ import type { ChannelListProps } from '../ChannelList'; type SetChannels = Dispatch>; -type BaseParameters = { - event: Event; +type BaseParameters = { + event: E; setChannels: SetChannels; }; -type RepeatedParameters = { - customHandler?: ( - setChannels: BaseParameters['setChannels'], - event: BaseParameters['event'], - ) => void; +type RepeatedParameters = { + customHandler?: (setChannels: SetChannels, event: E) => void; }; -type HandleMessageNewParameters = BaseParameters & - RepeatedParameters & { +type HandleMessageNewParameters = BaseParameters> & + RepeatedParameters> & { allowNewMessagesFromUnfilteredChannels: boolean; lockChannelOrder: boolean; } & Required>; -type HandleNotificationMessageNewParameters = BaseParameters & - RepeatedParameters & { +type HandleNotificationMessageNewParameters = BaseParameters< + EventPayload<'notification.message_new'> +> & + RepeatedParameters> & { allowNewMessagesFromUnfilteredChannels: boolean; lockChannelOrder: boolean; } & Required>; -type HandleNotificationRemovedFromChannelParameters = BaseParameters & RepeatedParameters; +type HandleNotificationRemovedFromChannelParameters = BaseParameters< + EventPayload<'notification.removed_from_channel'> +> & + RepeatedParameters>; -type HandleNotificationAddedToChannelParameters = BaseParameters & - RepeatedParameters & { +type HandleNotificationAddedToChannelParameters = BaseParameters< + EventPayload<'notification.added_to_channel'> +> & + RepeatedParameters> & { allowNewMessagesFromUnfilteredChannels: boolean; lockChannelOrder: boolean; } & Required>; -type HandleChannelVisibleParameters = BaseParameters & - RepeatedParameters & +type HandleChannelVisibleParameters = BaseParameters> & + RepeatedParameters> & Required>; -type HandleMemberUpdatedParameters = BaseParameters & { +type HandleMemberUpdatedParameters = BaseParameters> & { lockChannelOrder: boolean; } & Required>; -type HandleChannelDeletedParameters = BaseParameters & RepeatedParameters; +type HandleChannelDeletedParameters = BaseParameters> & + RepeatedParameters>; -type HandleChannelHiddenParameters = BaseParameters & RepeatedParameters; +type HandleChannelHiddenParameters = BaseParameters> & + RepeatedParameters>; -type HandleChannelTruncatedParameters = BaseParameters & RepeatedParameters; +type HandleChannelTruncatedParameters = BaseParameters< + EventPayload<'channel.truncated'> +> & + RepeatedParameters>; -type HandleChannelUpdatedParameters = BaseParameters & RepeatedParameters; +type HandleChannelUpdatedParameters = BaseParameters> & + RepeatedParameters>; -type HandleUserPresenceChangedParameters = BaseParameters; +type HandleUserPresenceChangedParameters = BaseParameters< + EventPayload<'user.presence.changed'> +>; -const shared = ({ +const shared = ({ customHandler, event, setChannels, -}: BaseParameters & RepeatedParameters) => { +}: BaseParameters & RepeatedParameters) => { if (typeof customHandler === 'function') { return customHandler(setChannels, event); } diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts index f62e46a01c..7dfd374b39 100644 --- a/src/components/ChannelList/hooks/usePaginatedChannels.ts +++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts @@ -7,8 +7,7 @@ import type { ChannelFilters, ChannelOptions, ChannelSort, - ErrorFromResponse, - ParsedPredefinedFilterResponse, + StreamAPIError, StreamChat, } from 'stream-chat'; @@ -45,17 +44,6 @@ export type EffectiveQueryParams = { sort: ChannelSort; }; -/** - * The `predefined_filter` response carries sort as - * `[{ field, direction }]`. `ChannelSort` expects `[{ field: direction }]`. - */ -const mapPredefinedFilterSortToChannelSort = ( - sort: NonNullable, -): ChannelSort => - sort.map(({ direction = 1, field }) => ({ - [field]: direction, - })) as ChannelSort; - export const usePaginatedChannels = ( client: StreamChat, filters: ChannelFilters, @@ -123,10 +111,12 @@ export const usePaginatedChannels = ( ...options, }; - const channelQueryResponse = await client.queryChannels( - filters, - sort || {}, - newOptions, + const channelQueryResponse = await client.queryChannelsAndHydrate( + { + filter_conditions: filters, + sort: sort ?? [], + ...newOptions, + }, { withResponse: true }, ); @@ -147,7 +137,7 @@ export const usePaginatedChannels = ( ? (predefinedFilter.filter as ChannelFilters) : undefined; const nextResponseSort = predefinedFilter?.sort - ? mapPredefinedFilterSortToChannelSort(predefinedFilter.sort) + ? predefinedFilter.sort : undefined; setResponseFilters(nextResponseFilters); @@ -177,7 +167,7 @@ export const usePaginatedChannels = ( }); if (isFirstPage) { - setError(error as ErrorFromResponse); + setError(error as StreamAPIError); } } diff --git a/src/components/ChannelList/utils.ts b/src/components/ChannelList/utils.ts index 3f1c5bde2b..398a7525f1 100644 --- a/src/components/ChannelList/utils.ts +++ b/src/components/ChannelList/utils.ts @@ -1,4 +1,4 @@ -import type { Channel, ChannelSort, ChannelSortBase } from 'stream-chat'; +import type { Channel, ChannelSort } from 'stream-chat'; import type { ChannelListProps } from './ChannelList'; @@ -101,33 +101,16 @@ export const extractSortValue = ({ targetKey, }: { atIndex: number; - targetKey: keyof ChannelSortBase; - sort?: ChannelListProps['sort']; + targetKey: string; + sort?: ChannelSort; }) => { if (!sort) return null; - let option: null | ChannelSortBase = null; - if (Array.isArray(sort)) { - option = sort[atIndex] ?? null; - } else { - let index = 0; - for (const key in sort) { - if (index !== atIndex) { - index++; - continue; - } + const option = sort[atIndex] ?? null; - if (key !== targetKey) { - return null; - } + if (option?.field !== targetKey) return null; - option = sort; - - break; - } - } - - return option?.[targetKey] ?? null; + return option.direction ?? null; }; /** @@ -147,7 +130,7 @@ export const isChannelPinned = (channel: Channel) => { const membership = channel.state.membership; - return typeof membership.pinned_at === 'string'; + return typeof membership?.pinned_at === 'string'; }; /** @@ -158,5 +141,5 @@ export const isChannelArchived = (channel: Channel) => { const membership = channel.state.membership; - return typeof membership.archived_at === 'string'; + return typeof membership?.archived_at === 'string'; }; diff --git a/src/components/ChannelListItem/ChannelListItem.tsx b/src/components/ChannelListItem/ChannelListItem.tsx index 9ed0d9fac1..f40dadf00f 100644 --- a/src/components/ChannelListItem/ChannelListItem.tsx +++ b/src/components/ChannelListItem/ChannelListItem.tsx @@ -1,7 +1,7 @@ import throttle from 'lodash.throttle'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; -import type { Channel, Event, LocalMessage } from 'stream-chat'; +import type { Channel, EventPayload, LocalMessage } from 'stream-chat'; import { ChannelListItemUI as DefaultChannelListItemUI } from './ChannelListItemUI'; import { useIsChannelMuted } from './hooks/useIsChannelMuted'; @@ -111,24 +111,24 @@ export const ChannelListItem = (props: ChannelListItemProps) => { const { muted } = useIsChannelMuted(channel); useEffect(() => { - const handleEvent = (event: Event) => { + const handleEvent = (event: EventPayload<'notification.mark_read'>) => { if (!event.cid) return setUnread(0); if (channel.cid === event.cid) setUnread(0); }; - client.on('notification.mark_read', handleEvent); - return () => client.off('notification.mark_read', handleEvent); + const subscription = client.on('notification.mark_read', handleEvent); + return () => subscription.unsubscribe(); }, [channel, client]); useEffect(() => { - const handleEvent = (event: Event) => { + const handleEvent = (event: EventPayload<'notification.mark_unread'>) => { if (channel.cid !== event.cid) return; if (event.user?.id !== client.user?.id) return; setUnread(channel.countUnread()); }; - channel.on('notification.mark_unread', handleEvent); + const subscription = channel.on('notification.mark_unread', handleEvent); return () => { - channel.off('notification.mark_unread', handleEvent); + subscription.unsubscribe(); }; }, [channel, client]); @@ -150,7 +150,16 @@ export const ChannelListItem = (props: ChannelListItemProps) => { getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated), ); - const handleEvent = (event: Event) => { + const handleEvent = ( + event: EventPayload< + | 'message.new' + | 'message.updated' + | 'message.deleted' + | 'user.messages.deleted' + | 'message.undeleted' + | 'channel.truncated' + >, + ) => { const deletedMessagesInAnotherChannel = event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid; @@ -165,20 +174,17 @@ export const ChannelListItem = (props: ChannelListItemProps) => { refreshUnreadCount(); }; - channel.on('message.new', handleEvent); - channel.on('message.updated', handleEvent); - channel.on('message.deleted', handleEvent); - client.on('user.messages.deleted', handleEvent); - channel.on('message.undeleted', handleEvent); - channel.on('channel.truncated', handleEvent); + const subscriptions = [ + channel.on('message.new', handleEvent), + channel.on('message.updated', handleEvent), + channel.on('message.deleted', handleEvent), + client.on('user.messages.deleted', handleEvent), + channel.on('message.undeleted', handleEvent), + channel.on('channel.truncated', handleEvent), + ]; return () => { - channel.off('message.new', handleEvent); - channel.off('message.updated', handleEvent); - channel.off('message.deleted', handleEvent); - client.off('user.messages.deleted', handleEvent); - channel.off('message.undeleted', handleEvent); - channel.off('channel.truncated', handleEvent); + subscriptions.forEach((subscription) => subscription.unsubscribe()); }; }, [ channel, diff --git a/src/components/ChannelListItem/hooks/useIsUserMuted.ts b/src/components/ChannelListItem/hooks/useIsUserMuted.ts index 2753ecbb94..2f32434a52 100644 --- a/src/components/ChannelListItem/hooks/useIsUserMuted.ts +++ b/src/components/ChannelListItem/hooks/useIsUserMuted.ts @@ -6,7 +6,7 @@ export const useIsUserMuted = (targetUserId?: string) => { const { mutes } = useChatContext(); return useMemo( - () => !!targetUserId && mutes.some((mute) => mute.target.id === targetUserId), + () => !!targetUserId && mutes.some((mute) => mute.target?.id === targetUserId), [mutes, targetUserId], ); }; diff --git a/src/components/ChannelListItem/hooks/useMessageDeliveryStatus.ts b/src/components/ChannelListItem/hooks/useMessageDeliveryStatus.ts index 92f98a4269..9ef9a5be1a 100644 --- a/src/components/ChannelListItem/hooks/useMessageDeliveryStatus.ts +++ b/src/components/ChannelListItem/hooks/useMessageDeliveryStatus.ts @@ -1,5 +1,10 @@ import { useCallback, useEffect, useState } from 'react'; -import type { Channel, Event, LocalMessage, UserResponse } from 'stream-chat'; +import type { + Channel, + EventHandler, + LocalMessage, + UserResponse, +} from 'stream-chat'; import { useChatContext } from '../../../context'; @@ -59,7 +64,7 @@ export const useMessageDeliveryStatus = ({ }, [channel, client, isOwnMessage, lastMessage]); useEffect(() => { - const handleMessageNew = (event: Event) => { + const handleMessageNew: EventHandler<'message.new'> = (event) => { // the last message is not mine, so do not show the delivery status if (!isOwnMessage(event.message)) { return setMessageDeliveryStatus(undefined); @@ -67,16 +72,16 @@ export const useMessageDeliveryStatus = ({ return setMessageDeliveryStatus(MessageDeliveryStatus.SENT); }; - channel.on('message.new', handleMessageNew); + const subscription = channel.on('message.new', handleMessageNew); return () => { - channel.off('message.new', handleMessageNew); + subscription.unsubscribe(); }; }, [channel, isOwnMessage]); useEffect(() => { if (!isOwnMessage(lastMessage)) return; - const handleMessageDelivered = (event: Event) => { + const handleMessageDelivered: EventHandler<'message.delivered'> = (event) => { if ( event.user?.id !== client.user?.id && lastMessage && @@ -85,17 +90,17 @@ export const useMessageDeliveryStatus = ({ setMessageDeliveryStatus(MessageDeliveryStatus.DELIVERED); }; - const handleMarkRead = (event: Event) => { + const handleMarkRead: EventHandler<'message.read'> = (event) => { if (event.user?.id !== client.user?.id) setMessageDeliveryStatus(MessageDeliveryStatus.READ); }; - channel.on('message.delivered', handleMessageDelivered); - channel.on('message.read', handleMarkRead); + const deliveredSubscription = channel.on('message.delivered', handleMessageDelivered); + const readSubscription = channel.on('message.read', handleMarkRead); return () => { - channel.off('message.delivered', handleMessageDelivered); - channel.off('message.read', handleMarkRead); + deliveredSubscription.unsubscribe(); + readSubscription.unsubscribe(); }; }, [channel, client, isOwnMessage, lastMessage]); diff --git a/src/components/Chat/hooks/useChannelsQueryState.ts b/src/components/Chat/hooks/useChannelsQueryState.ts index 3af1c3bef0..b724dc41da 100644 --- a/src/components/Chat/hooks/useChannelsQueryState.ts +++ b/src/components/Chat/hooks/useChannelsQueryState.ts @@ -1,6 +1,6 @@ import type { Dispatch, SetStateAction } from 'react'; import { useState } from 'react'; -import type { APIErrorResponse, ErrorFromResponse } from 'stream-chat'; +import type { APIErrorResponse, StreamAPIError } from 'stream-chat'; type ChannelQueryState = | 'uninitialized' // the initial state before the first channels query is triggered @@ -9,14 +9,14 @@ type ChannelQueryState = | null; // at least one channels page has been loaded and there is no query in progress at the moment export interface ChannelsQueryState { - error: ErrorFromResponse | null; + error: StreamAPIError | null; queryInProgress: ChannelQueryState; - setError: Dispatch | null>>; + setError: Dispatch | null>>; setQueryInProgress: Dispatch>; } export const useChannelsQueryState = (): ChannelsQueryState => { - const [error, setError] = useState | null>(null); + const [error, setError] = useState | null>(null); const [queryInProgress, setQueryInProgress] = useState('uninitialized'); diff --git a/src/components/Chat/hooks/useCreateChatClient.ts b/src/components/Chat/hooks/useCreateChatClient.ts index a4d461e832..dbd39900b0 100644 --- a/src/components/Chat/hooks/useCreateChatClient.ts +++ b/src/components/Chat/hooks/useCreateChatClient.ts @@ -1,13 +1,6 @@ import { useEffect, useState } from 'react'; - import { StreamChat } from 'stream-chat'; - -import type { - OwnUserResponse, - StreamChatOptions, - TokenOrProvider, - UserResponse, -} from 'stream-chat'; +import type { ClientUser, StreamChatOptions, TokenOrProvider } from 'stream-chat'; /** * React hook to create, connect and return `StreamChat` client. @@ -20,7 +13,7 @@ export const useCreateChatClient = ({ }: { apiKey: string; tokenOrProvider: TokenOrProvider; - userData: OwnUserResponse | UserResponse; + userData: ClientUser; options?: StreamChatOptions; }) => { const [chatClient, setChatClient] = useState(null); @@ -33,7 +26,7 @@ export const useCreateChatClient = ({ const [cachedOptions] = useState(options); useEffect(() => { - const client = new StreamChat(apiKey, undefined, cachedOptions); + const client = new StreamChat(apiKey, cachedOptions); let didUserConnectInterrupt = false; const connectionPromise = client diff --git a/src/components/Chat/hooks/useReportLostConnectionSystemNotification.ts b/src/components/Chat/hooks/useReportLostConnectionSystemNotification.ts index fc271c4a12..15d47de676 100644 --- a/src/components/Chat/hooks/useReportLostConnectionSystemNotification.ts +++ b/src/components/Chat/hooks/useReportLostConnectionSystemNotification.ts @@ -1,4 +1,5 @@ import { useEffect, useRef } from 'react'; +import type { EventPayload } from 'stream-chat'; import { useChatContext } from '../../../context/ChatContext'; import { useTranslationContext } from '../../../context/TranslationContext'; @@ -23,7 +24,7 @@ export const useReportLostConnectionSystemNotification = () => { connectionLostNotificationIdRef.current = null; }; - const handleConnectionChanged = ({ online }: { online?: boolean }) => { + const handleConnectionChanged = ({ online }: EventPayload<'connection.changed'>) => { if (!online) { if (connectionLostNotificationIdRef.current) return; @@ -40,10 +41,10 @@ export const useReportLostConnectionSystemNotification = () => { dismissConnectionLostNotification(); }; - client.on('connection.changed', handleConnectionChanged); + const subscription = client.on('connection.changed', handleConnectionChanged); return () => { - client.off('connection.changed', handleConnectionChanged); + subscription.unsubscribe(); dismissConnectionLostNotification(); }; }, [addSystemNotification, client, removeNotification, t]); diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorder.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorder.tsx index 1de5232459..90122cebdc 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorder.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorder.tsx @@ -26,10 +26,10 @@ export const AudioRecorder = () => {
{(isStopped(recordingState) || state.paused) && recording?.asset_url ? ( ) : state.recording ? ( diff --git a/src/components/MediaRecorder/classes/MediaRecorderController.ts b/src/components/MediaRecorder/classes/MediaRecorderController.ts index 4ac150abc4..b63cf76724 100644 --- a/src/components/MediaRecorder/classes/MediaRecorderController.ts +++ b/src/components/MediaRecorder/classes/MediaRecorderController.ts @@ -174,19 +174,21 @@ export class MediaRecorderController { return { asset_url: this.recordingUri, - duration: this.durationMs / 1000, - file_size: blob.size, + custom: { + duration: this.durationMs / 1000, + file_size: blob.size, + mime_type: blob.type, + waveform_data: resampleWaveformData( + this.amplitudeRecorder?.amplitudes.value ?? [], + this.amplitudeRecorderConfig.sampleCount, + ), + }, localMetadata: { file, id: nanoid(), }, - mime_type: blob.type, title: file.name, type: RecordingAttachmentType.VOICE_RECORDING, - waveform_data: resampleWaveformData( - this.amplitudeRecorder?.amplitudes.value ?? [], - this.amplitudeRecorderConfig.sampleCount, - ), } as LocalVoiceRecordingAttachment; }; diff --git a/src/components/Message/MessageAlsoSentInChannelIndicator.tsx b/src/components/Message/MessageAlsoSentInChannelIndicator.tsx index 29d9fe48c6..2febfcd54a 100644 --- a/src/components/Message/MessageAlsoSentInChannelIndicator.tsx +++ b/src/components/Message/MessageAlsoSentInChannelIndicator.tsx @@ -24,12 +24,18 @@ export const MessageAlsoSentInChannelIndicator = () => { const queryParent = () => channel .getClient() - .search({ cid: channel.cid }, { id: message.parent_id }) + .search({ + payload: { + filter_conditions: { cid: channel.cid }, + message_filter_conditions: { id: message.parent_id }, + }, + }) .then(({ results }) => { if (!results.length) { throw new Error('Thread has not been found'); } - targetMessageRef.current = formatMessage(results[0].message); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + targetMessageRef.current = formatMessage(results[0].message!); }) .catch((error: Error) => { addNotification({ diff --git a/src/components/Message/hooks/useReactionHandler.ts b/src/components/Message/hooks/useReactionHandler.ts index 87f9a52ad6..478af4d645 100644 --- a/src/components/Message/hooks/useReactionHandler.ts +++ b/src/components/Message/hooks/useReactionHandler.ts @@ -32,7 +32,7 @@ export const useReactionHandler = (message?: LocalMessage) => { const hasReaction = !!newReactionGroups[reactionType]; if (add) { - const timestamp = new Date().toISOString(); + const timestamp = new Date(); newReactionGroups[reactionType] = hasReaction ? { ...newReactionGroups[reactionType], @@ -42,6 +42,7 @@ export const useReactionHandler = (message?: LocalMessage) => { count: 1, first_reaction_at: timestamp, last_reaction_at: timestamp, + latest_reactions_by: [], sum_scores: 1, }; } else { @@ -99,11 +100,14 @@ export const useReactionHandler = (message?: LocalMessage) => { thread?.upsertReplyLocally({ message: tempMessage }); const messageResponse = add - ? await channel.sendReaction(id, { - type, - ...(emojiCode && { emoji_code: emojiCode }), - } as Reaction) - : await channel.deleteReaction(id, type); + ? await channel.sendReaction({ + id, + reaction: { + type, + ...(emojiCode && { emoji_code: emojiCode }), + } as Reaction, + }) + : await channel.deleteReaction({ id, type }); // seems useless as we're expecting WS event to come in and replace this anyway updateMessage(messageResponse.message); diff --git a/src/components/Message/hooks/useUserRole.ts b/src/components/Message/hooks/useUserRole.ts index 69babdd82b..73f2e22054 100644 --- a/src/components/Message/hooks/useUserRole.ts +++ b/src/components/Message/hooks/useUserRole.ts @@ -11,13 +11,13 @@ export const useUserRole = (message: LocalMessage, disableQuotedMessages?: boole * `isAdmin` will be removed in future release. See `channelCapabilities`. */ const isAdmin = - client.user?.role === 'admin' || channel.state.membership.role === 'admin'; + client.user?.role === 'admin' || channel.state.membership?.role === 'admin'; /** * @deprecated as it relies on `membership.role` check which is already deprecated and shouldn't be used anymore. * `isOwner` will be removed in future release. See `channelCapabilities`. */ - const isOwner = channel.state.membership.role === 'owner'; + const isOwner = channel.state.membership?.role === 'owner'; /** * @deprecated as it relies on `membership.role` check which is already deprecated and shouldn't be used anymore. @@ -25,12 +25,12 @@ export const useUserRole = (message: LocalMessage, disableQuotedMessages?: boole */ const isModerator = client.user?.role === 'channel_moderator' || - channel.state.membership.role === 'channel_moderator' || - channel.state.membership.role === 'moderator' || - channel.state.membership.is_moderator === true || - channel.state.membership.channel_role === 'channel_moderator'; + channel.state.membership?.role === 'channel_moderator' || + channel.state.membership?.role === 'moderator' || + channel.state.membership?.is_moderator === true || + channel.state.membership?.channel_role === 'channel_moderator'; - const isMyMessage = client.userID === message.user?.id; + const isMyMessage = client.userId === message.user?.id; const canEdit = !message.poll && diff --git a/src/components/MessageActions/MessageActions.defaults.tsx b/src/components/MessageActions/MessageActions.defaults.tsx index 22daec1697..1062ec759f 100644 --- a/src/components/MessageActions/MessageActions.defaults.tsx +++ b/src/components/MessageActions/MessageActions.defaults.tsx @@ -459,7 +459,7 @@ const DefaultMessageActionComponents = { type: 'api:message:saveForLater:delete:success', }); } else { - await client.reminders.createReminder({ messageId: message.id }); + await client.reminders.createReminder({ message_id: message.id }); addNotification({ context: { message, @@ -671,7 +671,7 @@ const DefaultMessageActionComponents = { onClick={() => { const targetId = message.user?.id; if (targetId) { - if (isBlocked) client.unBlockUser(targetId); + if (isBlocked) client.unblockUser(targetId); else client.blockUser(targetId); } closeMenu(); diff --git a/src/components/MessageActions/RemindMeSubmenu.tsx b/src/components/MessageActions/RemindMeSubmenu.tsx index 6c0214dd57..1b5bcf2b38 100644 --- a/src/components/MessageActions/RemindMeSubmenu.tsx +++ b/src/components/MessageActions/RemindMeSubmenu.tsx @@ -54,8 +54,8 @@ export const RemindMeSubmenu = () => { onClick={async () => { try { await client.reminders.upsertReminder({ - messageId: message.id, - remind_at: new Date(new Date().getTime() + offsetMs).toISOString(), + message_id: message.id, + remind_at: new Date(new Date().getTime() + offsetMs), }); addNotification({ context: { diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index d924ba248b..064b018af9 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -45,14 +45,16 @@ export const AudioAttachmentPreview = ({ const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; const url = attachment.asset_url || previewUri; + const { duration, file_size, mime_type, waveform_data } = + (attachment as LocalVoiceRecordingAttachment).custom; const audioPlayer = useAudioPlayer({ - fileSize: attachment.localMetadata.file?.size ?? attachment.file_size, - mimeType: attachment.localMetadata.file?.type ?? attachment.mime_type, + fileSize: attachment.localMetadata.file?.size ?? file_size, + mimeType: attachment.localMetadata.file?.type ?? mime_type, requester: attachment.localMetadata.id, src: url, title: attachment.title, - waveformData: attachment.waveform_data, + waveformData: waveform_data, }); useEffect(() => { @@ -65,7 +67,7 @@ export const AudioAttachmentPreview = ({ const { canPlayRecord, isPlaying, playbackRate, progressPercent, secondsElapsed } = useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {}; - const resolvedDuration = audioPlayer?.durationSeconds ?? attachment.duration; + const resolvedDuration = audioPlayer?.durationSeconds ?? duration; const hasWaveform = !!audioPlayer?.waveformData?.length; const hasSizeLimitError = uploadPermissionCheck?.reason === 'size_limit'; diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index 225f12d5a2..2340348b23 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -34,7 +34,7 @@ export const FileAttachmentPreview = ({ data-testid='attachment-preview-file' >
- +
diff --git a/src/components/MessageComposer/hooks/useSubmitHandler.ts b/src/components/MessageComposer/hooks/useSubmitHandler.ts index d6cb4a87c9..4c8ae0c71e 100644 --- a/src/components/MessageComposer/hooks/useSubmitHandler.ts +++ b/src/components/MessageComposer/hooks/useSubmitHandler.ts @@ -79,11 +79,17 @@ export const useSubmitHandler = (props: MessageComposerProps) => { await overrideSubmitHandler({ cid: messageComposer.channel.cid, localMessage, + // @ts-expect-error OAPI misalignment message, sendOptions, }); } else { - await sendMessage({ localMessage, message, options: sendOptions }); + await sendMessage({ + localMessage, + // @ts-expect-error OAPI misalignment + message, + options: sendOptions, + }); } if (messageComposer.config.text.publishTypingEvents) await messageComposer.channel.stopTyping(); diff --git a/src/components/MessageList/ScrollToLatestMessageButton.tsx b/src/components/MessageList/ScrollToLatestMessageButton.tsx index 99d7c5de3b..ed66696744 100644 --- a/src/components/MessageList/ScrollToLatestMessageButton.tsx +++ b/src/components/MessageList/ScrollToLatestMessageButton.tsx @@ -6,7 +6,7 @@ import { useTranslationContext, } from '../../context'; -import type { Event } from 'stream-chat'; +import type { EventPayload } from 'stream-chat'; import { Badge } from '../Badge'; import { Button } from '../Button'; import { IconArrowDown } from '../Icons'; @@ -37,7 +37,7 @@ const UnMemoizedScrollToLatestMessageButton = ( const observedEvent = threadList ? 'message.updated' : 'message.new'; useEffect(() => { - const handleEvent = (event: Event) => { + const handleEvent = (event: EventPayload<'message.new' | 'message.updated'>) => { const newMessageInAnotherChannel = event.cid !== activeChannel?.cid; const newMessageIsMine = event.user?.id === client.user?.id; @@ -63,10 +63,10 @@ const UnMemoizedScrollToLatestMessageButton = ( setCountUnread(() => newReplyCount - replyCount); } }; - client.on(observedEvent, handleEvent); + const subscription = client.on(observedEvent, handleEvent); return () => { - client.off(observedEvent, handleEvent); + subscription.unsubscribe(); }; }, [ activeChannel, diff --git a/src/components/MessageList/hooks/VirtualizedMessageList/useGiphyPreview.ts b/src/components/MessageList/hooks/VirtualizedMessageList/useGiphyPreview.ts index bf865e7d34..6607aa92e4 100644 --- a/src/components/MessageList/hooks/VirtualizedMessageList/useGiphyPreview.ts +++ b/src/components/MessageList/hooks/VirtualizedMessageList/useGiphyPreview.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useChatContext } from '../../../../context/ChatContext'; -import type { EventHandler, LocalMessage } from 'stream-chat'; +import type { EventPayload, LocalMessage } from 'stream-chat'; export const useGiphyPreview = (separateGiphyPreview: boolean) => { const [giphyPreviewMessage, setGiphyPreviewMessage] = useState(); @@ -11,16 +11,16 @@ export const useGiphyPreview = (separateGiphyPreview: boolean) => { useEffect(() => { if (!separateGiphyPreview) return; - const handleEvent: EventHandler = (event) => { + const handleEvent = (event: EventPayload<'message.new'>) => { const { message, user } = event; - if (message?.command === 'giphy' && user?.id === client.userID) { + if (message?.command === 'giphy' && user?.id === client.userId) { setGiphyPreviewMessage(undefined); } }; - client.on('message.new', handleEvent); - return () => client.off('message.new', handleEvent); + const subscription = client.on('message.new', handleEvent); + return () => subscription.unsubscribe(); }, [client, separateGiphyPreview]); return { diff --git a/src/components/MessageList/hooks/useMarkRead.ts b/src/components/MessageList/hooks/useMarkRead.ts index 5307f562ac..17edc4af22 100644 --- a/src/components/MessageList/hooks/useMarkRead.ts +++ b/src/components/MessageList/hooks/useMarkRead.ts @@ -4,7 +4,12 @@ import { useChannelStateContext, useChatContext, } from '../../../context'; -import type { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat'; +import type { + Channel, + EventPayload, + LocalMessage, + MessageResponse, +} from 'stream-chat'; const hasReadLastMessage = (channel: Channel, userId: string) => { const latestMessageIdInChannel = channel.state.latestMessages.slice(-1)[0]?.id; @@ -50,7 +55,7 @@ export const useMarkRead = ({ if (shouldMarkRead()) markRead(); }; - const handleMessageNew = (event: Event) => { + const handleMessageNew = (event: EventPayload<'message.new'>) => { const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; @@ -80,7 +85,7 @@ export const useMarkRead = ({ } }; - channel.on('message.new', handleMessageNew); + const subscription = channel.on('message.new', handleMessageNew); document.addEventListener('visibilitychange', onVisibilityChange); if (shouldMarkRead()) { @@ -88,7 +93,7 @@ export const useMarkRead = ({ } return () => { - channel.off('message.new', handleMessageNew); + subscription.unsubscribe(); document.removeEventListener('visibilitychange', onVisibilityChange); }; }, [ diff --git a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx index 15accc7e2d..bf76bb614e 100644 --- a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx +++ b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx @@ -46,7 +46,8 @@ export const SuggestPollOptionPrompt = () => { const onSubmit = useCallback( async (formValue: { optionText: string }) => { - await client.createPollOption(poll.id, { + await client.createPollOption({ + poll_id: poll.id, text: formValue.optionText, }); close(); diff --git a/src/components/Poll/PollHeader.tsx b/src/components/Poll/PollHeader.tsx index c355a35b79..1ecca60a82 100644 --- a/src/components/Poll/PollHeader.tsx +++ b/src/components/Poll/PollHeader.tsx @@ -6,7 +6,7 @@ import type { PollOption, PollState } from 'stream-chat'; type PollStateSelectorReturnValue = { enforce_unique_vote: boolean; is_closed: boolean | undefined; - max_votes_allowed: number; + max_votes_allowed: number | undefined; name: string; options: PollOption[]; }; diff --git a/src/components/Poll/PollOptionSelector.tsx b/src/components/Poll/PollOptionSelector.tsx index 189c104d6f..5870186b28 100644 --- a/src/components/Poll/PollOptionSelector.tsx +++ b/src/components/Poll/PollOptionSelector.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import React, { useMemo } from 'react'; -import type { PollOption, PollState, PollVote, VotingVisibility } from 'stream-chat'; +import type { PollOption, PollState, PollVote } from 'stream-chat'; import { isVoteAnswer } from 'stream-chat'; import { AvatarStack as DefaultAvatarStack } from '../Avatar'; import { @@ -38,7 +38,7 @@ type PollStateSelectorReturnValue = { maxVotedOptionIds: string[]; ownVotesByOptionId: Record; vote_counts_by_option: Record; - voting_visibility: VotingVisibility | undefined; + voting_visibility: string; }; const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue => ({ is_closed: nextValue.is_closed, diff --git a/src/components/Poll/PollVote.tsx b/src/components/Poll/PollVote.tsx index f10cefdcfc..6fa1808c08 100644 --- a/src/components/Poll/PollVote.tsx +++ b/src/components/Poll/PollVote.tsx @@ -6,12 +6,12 @@ import { useChatContext, useTranslationContext } from '../../context'; import type { PollVote as PollVoteType } from 'stream-chat'; -const PollVoteTimestamp = ({ timestamp }: { timestamp: string }) => { +const PollVoteTimestamp = ({ timestamp }: { timestamp: Date | string }) => { const { t } = useTranslationContext(); const { handleEnter, handleLeave, tooltipVisible } = useEnterLeaveHandlers(); const [referenceElement, setReferenceElement] = useState(null); - const timestampDate = new Date(timestamp); + const timestampDate = timestamp instanceof Date ? timestamp : new Date(timestamp); return (
{ - const handleVoteEvent = (event: Event) => { + const handleVoteEvent = ( + event: EventPayload<'poll.vote_casted' | 'poll.vote_changed' | 'poll.vote_removed'>, + ) => { if (!event.poll_vote) return; const isAnswer = isVoteAnswer(event.poll_vote); if ( diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index 689a899c41..6f5de3e802 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -28,7 +28,7 @@ export type MessageReactionsDetailProps = Partial< reactionGroups?: ReturnType['reactionGroups']; } & ReactionSelectorProps; -const defaultReactionDetailsSort = { created_at: -1 } as const; +const defaultReactionDetailsSort: ReactionSort = [{ direction: -1, field: 'created_at' }]; export const MessageReactionsDetailLoadingIndicator = () => { const elements = useMemo( diff --git a/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts index 6796680afb..bcc1002e4b 100644 --- a/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts +++ b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import type { Attachment, LocalMessage } from 'stream-chat'; +import type { Attachment, LocalMessage, VoiceRecordingAttachment } from 'stream-chat'; import { isAudioAttachment, isFileAttachment, @@ -226,9 +226,11 @@ export const useLatestMessagePreview = ({ getAttachmentFallbackText(contentType, attachments.length, t); // attach duration for audio/video attachments if available - if (attachments.length === 1 && typeof firstAttachment.duration === 'number') { - const minutes = Math.floor(firstAttachment.duration / 60); - const seconds = Math.ceil(firstAttachment.duration) % 60; + const firstAttachmentDuration = (firstAttachment as VoiceRecordingAttachment).custom + .duration; + if (attachments.length === 1 && typeof firstAttachmentDuration === 'number') { + const minutes = Math.floor(firstAttachmentDuration / 60); + const seconds = Math.ceil(firstAttachmentDuration) % 60; const durationString = `${minutes}:${seconds.toString().padStart(2, '0')}`; text += ` (${durationString})`; } diff --git a/src/context/ChatContext.tsx b/src/context/ChatContext.tsx index 83af30a3de..b10ef1b364 100644 --- a/src/context/ChatContext.tsx +++ b/src/context/ChatContext.tsx @@ -1,11 +1,6 @@ import React, { useContext } from 'react'; import type { PropsWithChildren } from 'react'; -import type { - AppSettingsAPIResponse, - Channel, - Mute, - SearchController, -} from 'stream-chat'; +import type { Channel, Mute, SearchController, StreamChat } from 'stream-chat'; import type { ChatProps } from '../components/Chat/Chat'; import type { ChannelsQueryState } from '../components/Chat/hooks/useChannelsQueryState'; @@ -31,7 +26,7 @@ export type ChatContextValue = { * Indicates, whether a channels query has been triggered within ChannelList by its channels pagination controller. */ channelsQueryState: ChannelsQueryState; - getAppSettings: () => Promise | null; + getAppSettings: () => ReturnType | null; latestMessageDatesByChannels: Record; mutes: Array; /** Instance of SearchController class that allows to control all the search operations. */ diff --git a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx index fdf71a0bfe..90b2ccbfd8 100644 --- a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx @@ -70,6 +70,7 @@ const ChannelFileListItem = ({ item }: { item: ChannelFileItem }) => { const { attachment } = item; const fileName = getAttachmentFileName(attachment); const assetUrl = attachment.asset_url; + const { file_size, mime_type } = attachment.custom; const LeadingSlot = useMemo( () => @@ -78,23 +79,23 @@ const ChannelFileListItem = ({ item }: { item: ChannelFileItem }) => { ); }, - [attachment.mime_type, fileName], + [mime_type, fileName], ); const sharedProps = useMemo( () => ({ LeadingSlot, - subtitle: , + subtitle: , subtitleClassName: 'str-chat__channel-detail__files-view__list-item__size', title: fileName, titleClassName: 'str-chat__channel-detail__files-view__list-item__name', }), - [attachment.file_size, fileName, LeadingSlot], + [file_size, fileName, LeadingSlot], ); const linkRootProps = useMemo( diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx index 8c04cbeb38..90ef9fb37a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx @@ -316,7 +316,7 @@ const UserMuteAction = () => { const { addNotification } = useNotificationApi(); const { t } = useTranslationContext(); const otherMember = useOtherMember(); - const userMuted = !!mutes.find((mute) => mute.target.id === otherMember?.user?.id); + const userMuted = !!mutes.find((mute) => mute.target?.id === otherMember?.user?.id); const [optimisticUserMuted, setOptimisticUserMuted] = useState(userMuted); useEffect(() => { @@ -464,7 +464,7 @@ const BlockUserAction = () => { try { setUserBlockInProgress(true); - await client.unBlockUser(targetUserId); + await client.unblockUser(targetUserId); addNotification({ context: { channel }, emitter: 'ChannelManagementView', diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx index b4979b38f1..f4afa919dd 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx @@ -173,7 +173,7 @@ const useChannelManagementEditForm = ({ // Dirty-tracking baseline; advanced to the saved value on success so the form // is no longer considered dirty (and the Save button hides) after a write. - const [baselineName, setBaselineName] = useState(channel.data?.name ?? ''); + const [baselineName, setBaselineName] = useState(channel.data?.custom?.name ?? ''); const [name, setName] = useState(baselineName); // null = keep current avatar, File = replace it, 'removed' = clear it const [imageEdit, setImageEdit] = useState(null); diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.ts b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.ts index 572aa93da4..34e333ce70 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.ts +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.ts @@ -5,6 +5,7 @@ import { type LocalMessage, type MessageResponse, type UserResponse, + type VoiceRecordingAttachment, } from 'stream-chat'; import { toBaseImageDescriptors } from '../../../../components/BaseImage'; @@ -58,9 +59,10 @@ export const toChannelMediaItems = ( : Boolean(descriptor.imageUrl); if (!hasRenderableSource) return; + const attachmentDuration = (attachment as VoiceRecordingAttachment).custom.duration; items.push({ durationSeconds: - typeof attachment.duration === 'number' ? attachment.duration : undefined, + typeof attachmentDuration === 'number' ? attachmentDuration : undefined, galleryItem: descriptor, id: `${message.id}-${index}`, type, diff --git a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx index afcc3ff38e..3c45a2c77f 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx @@ -222,7 +222,7 @@ const SendDirectMessageAction = () => { setIsSending(true); try { const directMessageChannel = client.channel(channel.type, { - members: [client.userID, targetUserId], + members: [{ user_id: client.userID }, { user_id: targetUserId }], }); await directMessageChannel.watch(); setActiveChannel(directMessageChannel); @@ -278,7 +278,7 @@ const UserMuteAction = () => { const { t } = useTranslationContext(); const { targetUserId } = useChannelMemberActionContext(); const userMuted = - !!targetUserId && mutes.some((mute) => mute.target.id === targetUserId); + !!targetUserId && mutes.some((mute) => mute.target?.id === targetUserId); const [optimisticUserMuted, setOptimisticUserMuted] = useState(userMuted); useEffect(() => { @@ -415,7 +415,7 @@ const BlockUserAction = () => { try { setUserBlockInProgress(true); - await client.unBlockUser(targetUserId); + await client.unblockUser(targetUserId); addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx index 711620f956..6010e3f9b4 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx @@ -158,7 +158,7 @@ export const ChannelMembersAddView = ({ const selectedUserIdSet = useMemo(() => new Set(selectedUserIds), [selectedUserIds]); const mutedUserIdSet = useMemo( - () => new Set(mutes.map((mute) => mute.target.id)), + () => new Set(mutes.map((mute) => mute.target?.id)), [mutes], ); diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx index 52dda9f8ce..cd9d139303 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx @@ -137,7 +137,7 @@ export const ChannelMembersBrowseView = ({ searchInputResetKey, } = useChannelMembersSearch(); const mutedUserIdSet = useMemo( - () => new Set(mutes.map((mute) => mute.target.id)), + () => new Set(mutes.map((mute) => mute.target?.id)), [mutes], ); diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts index 4a832e3102..bb4e6c54da 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts @@ -9,7 +9,11 @@ export const getMemberUserId = (member: ChannelMemberResponse) => member.user?.id || member.user_id; export const getUserDisplayName = (user?: UserResponse) => - user?.name || user?.username || user?.id || ''; + user?.name || + // @ts-expect-error username is not typed + user?.custom.username || + user?.id || + ''; export const getChannelMemberUserIds = (channel: Channel) => Object.values(channel.state?.members ?? {}) diff --git a/src/utils/getChannel.ts b/src/utils/getChannel.ts index 1928020521..d88cfc9b06 100644 --- a/src/utils/getChannel.ts +++ b/src/utils/getChannel.ts @@ -44,8 +44,12 @@ export const getChannel = async ({ } // unfortunately typescript is not able to infer that if (!channel && !type) === false, then channel or type has to be truthy - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const theChannel = channel || client.channel(type!, id, { members }); + const theChannel = + channel || + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + client.channel(type!, id, { + members: members?.map((userId) => ({ user_id: userId })), + }); // need to keep as with call to channel.watch the id can be changed from undefined to an actual ID generated server-side const originalCid = theChannel?.id From 21b56f439dfd13c77d96c4dcfcab9df6c00267f6 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 7 Jul 2026 18:19:05 +0200 Subject: [PATCH 2/5] Second pass --- .../hooks/useIncomingMessageAnnouncements.ts | 12 ++++--- src/components/Attachment/Attachment.tsx | 13 ++++--- .../Attachment/AttachmentContainer.tsx | 8 ++--- src/components/Attachment/Geolocation.tsx | 6 ++-- src/components/Attachment/utils.tsx | 12 ++++--- src/components/Channel/Channel.tsx | 6 ++-- .../Channel/hooks/useEditMessageHandler.ts | 21 +++++------- src/components/Channel/utils.ts | 24 ++++++------- src/components/ChannelList/ChannelListUI.tsx | 4 +-- .../ChannelList/hooks/usePaginatedChannels.ts | 4 +-- src/components/ChannelListItem/utils.tsx | 8 ++--- .../Chat/hooks/useChannelsQueryState.ts | 8 ++--- src/components/Chat/hooks/useChat.ts | 15 ++++---- src/components/Message/hooks/usePinHandler.ts | 10 +++--- .../Message/hooks/useReactionsFetcher.ts | 11 +++--- src/components/Message/utils.tsx | 34 +++++++------------ .../MessageActions/DownloadSubmenu.tsx | 4 ++- .../AttachmentSelector/CommandsMenu.tsx | 6 ++-- .../MessageComposer/QuotedMessagePreview.tsx | 22 +++++++----- .../hooks/useMessageComposerCommands.ts | 6 ++-- .../PollResults/PollOptionWithVotes.tsx | 4 +-- src/components/Poll/PollOptionSelector.tsx | 8 ++--- src/components/Poll/PollVote.tsx | 2 +- .../Poll/hooks/useManagePollVotesRealtime.ts | 4 +-- .../Poll/hooks/usePollAnswerPagination.ts | 13 ++++--- .../hooks/usePollOptionVotesPagination.ts | 10 +++--- .../Search/SearchResults/SearchResultItem.tsx | 18 +++++++--- .../SuggestionList/CommandItem.tsx | 8 ++--- src/context/ChannelStateContext.tsx | 4 +-- src/context/ChatContext.tsx | 9 +++-- src/context/MessageContext.tsx | 4 +-- src/utils/getChannel.ts | 4 +-- 32 files changed, 167 insertions(+), 155 deletions(-) diff --git a/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts b/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts index a8b1ceb129..b6ac31f05d 100644 --- a/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts +++ b/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef } from 'react'; -import type { Channel, Event, MessageResponse } from 'stream-chat'; +import type { Channel, EventPayload, LocalMessage, MessageResponse } from 'stream-chat'; import { useAriaLiveAnnouncer } from '../useAriaLiveAnnouncer'; import { useTranslationContext } from '../../../context/TranslationContext'; @@ -7,7 +7,7 @@ import { useTranslationContext } from '../../../context/TranslationContext'; const MESSAGE_ANNOUNCEMENT_THROTTLE_MS = 1000; const isAnnounceableIncomingMessage = ( - message: MessageResponse, + message: LocalMessage | MessageResponse, ownUserId?: string, ): boolean => { const messageUserId = message.user?.id; @@ -16,13 +16,15 @@ const isAnnounceableIncomingMessage = ( return false; } + // TODO: message coming from the event does not have a status + const status = (message as LocalMessage).status; return ( message.type !== 'deleted' && message.type !== 'ephemeral' && message.type !== 'error' && message.type !== 'system' && - message.status !== 'failed' && - message.status !== 'sending' + status !== 'failed' && + status !== 'sending' ); }; @@ -108,7 +110,7 @@ export const useIncomingMessageAnnouncements = ({ return; } - const handleMessageNew = (event: Event) => { + const handleMessageNew = (event: EventPayload<'message.new'>) => { const message = event.message; if (!message) return; diff --git a/src/components/Attachment/Attachment.tsx b/src/components/Attachment/Attachment.tsx index 2f38d3236b..801e57a619 100644 --- a/src/components/Attachment/Attachment.tsx +++ b/src/components/Attachment/Attachment.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from 'react'; -import type { SharedLocationResponse, Attachment as StreamAttachment } from 'stream-chat'; +import type { SharedLocationResponseData, Attachment as StreamAttachment } from 'stream-chat'; import { isAudioAttachment, isFileAttachment, isImageAttachment, isScrapedContent, - isSharedLocationResponse, + isSharedLocationResponseData, isVideoAttachment, isVoiceRecordingAttachment, } from 'stream-chat'; @@ -29,7 +29,6 @@ import type { AudioProps } from './Audio'; import type { VoiceRecordingProps } from './VoiceRecording'; import type { CardProps } from './LinkPreview/Card'; import type { FileAttachmentProps } from './FileAttachment'; -import type { GalleryItem } from '../Gallery'; import type { UnsupportedAttachmentProps } from './UnsupportedAttachment'; import type { ActionHandlerReturnType } from '../Message/hooks/useActionHandler'; import type { GeolocationProps } from './Geolocation'; @@ -49,7 +48,7 @@ export const ATTACHMENT_GROUPS_ORDER = [ export type AttachmentProps = { /** The message attachments to render, see [attachment structure](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) **/ - attachments: (StreamAttachment | SharedLocationResponse)[]; + attachments: (StreamAttachment | SharedLocationResponseData)[]; /** The handler function to call when an action is performed on an attachment, examples include canceling a \/giphy command or shuffling the results. */ actionHandler?: ActionHandlerReturnType; /** @@ -120,10 +119,10 @@ const renderGroupedAttachments = ({ attachments, ...rest }: AttachmentProps): GroupedRenderedAttachment => { - const mediaAttachments: GalleryItem[] = []; + const mediaAttachments: StreamAttachment[] = []; const containers = attachments.reduce( (typeMap, attachment) => { - if (isSharedLocationResponse(attachment)) { + if (isSharedLocationResponseData(attachment)) { typeMap.geolocation.push( ; GeolocationMap?: ComponentType; }; @@ -95,7 +95,7 @@ export const Geolocation = ({ }; export type GeolocationAttachmentMapPlaceholderProps = { - location: SharedLocationResponse; + location: SharedLocationResponseData; }; const DefaultGeolocationAttachmentMapPlaceholder = ({ diff --git a/src/components/Attachment/utils.tsx b/src/components/Attachment/utils.tsx index 85d61d051a..1c07bbe298 100644 --- a/src/components/Attachment/utils.tsx +++ b/src/components/Attachment/utils.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import type { Attachment, SharedLocationResponse } from 'stream-chat'; +import type { Attachment, SharedLocationResponseData } from 'stream-chat'; import type { ATTACHMENT_GROUPS_ORDER, AttachmentProps } from './Attachment'; import * as linkify from 'linkifyjs'; @@ -44,18 +44,20 @@ export type RenderMediaProps = Omit & { }; export type GeolocationContainerProps = Omit & { - location: SharedLocationResponse; + location: SharedLocationResponseData; }; // This identity function determines attachment type specific to React. // Once made sure other SDKs support the same logic, move to stream-chat-js export const isGalleryAttachmentType = ( - attachment: Attachment | GalleryAttachment, + attachment: Attachment | GalleryAttachment | SharedLocationResponseData, ): attachment is GalleryAttachment => Array.isArray((attachment as GalleryAttachment).items); -export const isSvgAttachment = (attachment: Attachment) => { - const filename = attachment.fallback || ''; +export const isSvgAttachment = ( + attachment: Attachment | GalleryAttachment | SharedLocationResponseData, +) => { + const filename = (attachment as Attachment).fallback || ''; return filename.toLowerCase().endsWith('.svg'); }; diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 1c5e226508..d3ad62347b 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -12,10 +12,10 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import throttle from 'lodash.throttle'; import type { - ChannelAPIResponse, ChannelMemberResponse, ChannelQueryOptions, ChannelState, + ChannelStateResponseFields, DeleteMessageOptions, Event, EventAPIResponse, @@ -625,7 +625,7 @@ const ChannelInner = ( const oldestID = oldestMessage?.id; const perPage = limit; - let queryResponse: ChannelAPIResponse; + let queryResponse: ChannelStateResponseFields; try { queryResponse = await channel.query({ @@ -659,7 +659,7 @@ const ChannelInner = ( const newestId = newestMessage?.id; const perPage = limit; - let queryResponse: ChannelAPIResponse; + let queryResponse: ChannelStateResponseFields; try { queryResponse = await channel.query({ diff --git a/src/components/Channel/hooks/useEditMessageHandler.ts b/src/components/Channel/hooks/useEditMessageHandler.ts index 4e6004bb08..7605fc3395 100644 --- a/src/components/Channel/hooks/useEditMessageHandler.ts +++ b/src/components/Channel/hooks/useEditMessageHandler.ts @@ -1,30 +1,27 @@ -import type { - LocalMessage, - MessageResponse, - StreamChat, - UpdateMessageOptions, -} from 'stream-chat'; +import type { MessageRequest, StreamChat, UpdateMessageOptions } from 'stream-chat'; import { useChatContext } from '../../../context/ChatContext'; type UpdateHandler = ( cid: string, - updatedMessage: LocalMessage | MessageResponse, + updatedMessage: MessageRequest, options?: UpdateMessageOptions, ) => ReturnType; export const useEditMessageHandler = (doUpdateMessageRequest?: UpdateHandler) => { const { channel, client } = useChatContext('useEditMessageHandler'); - return ( - updatedMessage: LocalMessage | MessageResponse, - options?: UpdateMessageOptions, - ) => { + return (updatedMessage: MessageRequest, options?: UpdateMessageOptions) => { if (doUpdateMessageRequest && channel) { return Promise.resolve( doUpdateMessageRequest(channel.cid, updatedMessage, options), ); } - return client.updateMessage(updatedMessage, undefined, options); + return client.updateMessage({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: updatedMessage.id!, + message: updatedMessage, + ...options, + }); }; }; diff --git a/src/components/Channel/utils.ts b/src/components/Channel/utils.ts index b555c236c4..3743a88c3e 100644 --- a/src/components/Channel/utils.ts +++ b/src/components/Channel/utils.ts @@ -1,8 +1,8 @@ import { - type APIErrorResponse, + type APIError, type ChannelState, - ErrorFromResponse, type MessageResponse, + StreamAPIError, } from 'stream-chat'; /** @@ -77,27 +77,27 @@ export const findInMsgSetByDate = ( }; /** * Compatibility adapter: - * LocalMessage.error expects ErrorFromResponse, but some transport failures + * LocalMessage.error expects StreamAPIError, but some transport failures * (for example Axios ERR_NETWORK while offline) do not have an HTTP response payload. */ export const adaptMessageSendErrorToErrorFromResponse = ( error: unknown, -): ErrorFromResponse => { - if (error instanceof ErrorFromResponse) { +): StreamAPIError => { + if (error instanceof StreamAPIError) { return error; } const fallbackMessage = error instanceof Error ? error.message : 'Message send failed'; let message = fallbackMessage; let status = 0; - let code: number | null = null; + let code: number | undefined = undefined; if (typeof error === 'object' && error !== null) { const maybeAxiosError = error as { code?: unknown; message?: unknown; name?: unknown; - response?: ErrorFromResponse['response']; + response?: StreamAPIError['response']; status?: unknown; }; @@ -108,8 +108,8 @@ export const adaptMessageSendErrorToErrorFromResponse = ( : 'Network Error'; status = maybeAxiosError.response?.status ?? 0; - return new ErrorFromResponse(message, { - code: null, + return new StreamAPIError(message, { + code: undefined, response: maybeAxiosError.response ?? ({ @@ -121,7 +121,7 @@ export const adaptMessageSendErrorToErrorFromResponse = ( StatusCode: status, }, status, - } as ErrorFromResponse['response']), + } as unknown as StreamAPIError['response']), status, }); } @@ -147,7 +147,7 @@ export const adaptMessageSendErrorToErrorFromResponse = ( } } - return new ErrorFromResponse(message, { + return new StreamAPIError(message, { code, response: { // Compatibility shim: this is an intentionally incomplete AxiosResponse-like object. @@ -158,7 +158,7 @@ export const adaptMessageSendErrorToErrorFromResponse = ( StatusCode: status, }, status, - } as ErrorFromResponse['response'], + } as unknown as StreamAPIError['response'], status, }); }; diff --git a/src/components/ChannelList/ChannelListUI.tsx b/src/components/ChannelList/ChannelListUI.tsx index 5859c2a86d..da4de99e0d 100644 --- a/src/components/ChannelList/ChannelListUI.tsx +++ b/src/components/ChannelList/ChannelListUI.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; -import type { APIErrorResponse, Channel, StreamAPIError } from 'stream-chat'; +import type { APIError, Channel, StreamAPIError } from 'stream-chat'; import { LoadingChannels } from '../Loading/LoadingChannels'; import { NullComponent } from '../UtilityComponents'; @@ -8,7 +8,7 @@ import { useComponentContext, useTranslationContext } from '../../context'; export type ChannelListUIProps = { /** Whether the channel query request returned an errored response */ - error: StreamAPIError | null; + error: StreamAPIError | null; /** The channels currently loaded in the list, only defined if `sendChannelsToList` on `ChannelList` is true */ loadedChannels?: Channel[]; /** Whether the channels are currently loading */ diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts index 7dfd374b39..846cd36af6 100644 --- a/src/components/ChannelList/hooks/usePaginatedChannels.ts +++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import uniqBy from 'lodash.uniqby'; import type { - APIErrorResponse, + APIError, Channel, ChannelFilters, ChannelOptions, @@ -167,7 +167,7 @@ export const usePaginatedChannels = ( }); if (isFirstPage) { - setError(error as StreamAPIError); + setError(error as StreamAPIError); } } diff --git a/src/components/ChannelListItem/utils.tsx b/src/components/ChannelListItem/utils.tsx index c626d9fef0..06460e08c9 100644 --- a/src/components/ChannelListItem/utils.tsx +++ b/src/components/ChannelListItem/utils.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import React from 'react'; import ReactMarkdown from 'react-markdown'; -import type { Channel, PollVote } from 'stream-chat'; +import type { Channel, PollVoteResponseData } from 'stream-chat'; import type { ChatContextValue } from '../../context'; import { getTranslatedMessageText } from '../../context/MessageTranslationViewContext'; @@ -24,8 +24,8 @@ export const renderPreviewText = (text: string) => ( ); -const getLatestPollVote = (latestVotesByOption: Record) => { - let latestVote: PollVote | undefined; +const getLatestPollVote = (latestVotesByOption: Record) => { + let latestVote: PollVoteResponseData | undefined; for (const optionVotes of Object.values(latestVotesByOption)) { optionVotes.forEach((vote) => { if (latestVote && new Date(latestVote.updated_at) >= new Date(vote.created_at)) @@ -71,7 +71,7 @@ export const getLatestMessagePreview = ( }); } else { const latestVote = getLatestPollVote( - poll.latest_votes_by_option as Record, + poll.latest_votes_by_option as Record, ); const option = latestVote && poll.options.find((opt) => opt.id === latestVote.option_id); diff --git a/src/components/Chat/hooks/useChannelsQueryState.ts b/src/components/Chat/hooks/useChannelsQueryState.ts index b724dc41da..5e9eeea02c 100644 --- a/src/components/Chat/hooks/useChannelsQueryState.ts +++ b/src/components/Chat/hooks/useChannelsQueryState.ts @@ -1,6 +1,6 @@ import type { Dispatch, SetStateAction } from 'react'; import { useState } from 'react'; -import type { APIErrorResponse, StreamAPIError } from 'stream-chat'; +import type { APIError, StreamAPIError } from 'stream-chat'; type ChannelQueryState = | 'uninitialized' // the initial state before the first channels query is triggered @@ -9,14 +9,14 @@ type ChannelQueryState = | null; // at least one channels page has been loaded and there is no query in progress at the moment export interface ChannelsQueryState { - error: StreamAPIError | null; + error: StreamAPIError | null; queryInProgress: ChannelQueryState; - setError: Dispatch | null>>; + setError: Dispatch | null>>; setQueryInProgress: Dispatch>; } export const useChannelsQueryState = (): ChannelsQueryState => { - const [error, setError] = useState | null>(null); + const [error, setError] = useState | null>(null); const [queryInProgress, setQueryInProgress] = useState('uninitialized'); diff --git a/src/components/Chat/hooks/useChat.ts b/src/components/Chat/hooks/useChat.ts index 0af9603cfd..917084c422 100644 --- a/src/components/Chat/hooks/useChat.ts +++ b/src/components/Chat/hooks/useChat.ts @@ -10,12 +10,11 @@ import { } from '../../../i18n'; import type { - AppSettingsAPIResponse, Channel, - Event, - Mute, + EventPayload, OwnUserResponse, StreamChat, + UserMuteResponse, } from 'stream-chat'; export type UseChatParams = { @@ -36,12 +35,12 @@ export const useChat = ({ }); const [channel, setChannel] = useState(); - const [mutes, setMutes] = useState>([]); + const [mutes, setMutes] = useState>([]); const [latestMessageDatesByChannels, setLatestMessageDatesByChannels] = useState({}); const clientMutes = (client.user as OwnUserResponse)?.mutes ?? []; - const appSettings = useRef | null>(null); + const appSettings = useRef | null>(null); const getAppSettings = () => { if (appSettings.current) { @@ -79,12 +78,12 @@ export const useChat = ({ useEffect(() => { setMutes(clientMutes); - const handleEvent = (event: Event) => { + const handleEvent = (event: EventPayload<'notification.mutes_updated'>) => { setMutes(event.me?.mutes || []); }; - client.on('notification.mutes_updated', handleEvent); - return () => client.off('notification.mutes_updated', handleEvent); + const subscription = client.on('notification.mutes_updated', handleEvent); + return () => subscription.unsubscribe(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [clientMutes?.length]); diff --git a/src/components/Message/hooks/usePinHandler.ts b/src/components/Message/hooks/usePinHandler.ts index 6e9094eae2..ff15ed122e 100644 --- a/src/components/Message/hooks/usePinHandler.ts +++ b/src/components/Message/hooks/usePinHandler.ts @@ -2,7 +2,7 @@ import { useChannelActionContext } from '../../../context/ChannelActionContext'; import { useChannelStateContext } from '../../../context/ChannelStateContext'; import { useChatContext } from '../../../context/ChatContext'; -import type { LocalMessage } from 'stream-chat'; +import type { LocalMessage, UserResponse } from 'stream-chat'; import type { ReactEventHandler } from '../types'; export const usePinHandler = (message: LocalMessage) => { @@ -23,7 +23,7 @@ export const usePinHandler = (message: LocalMessage) => { ...message, pinned: true, pinned_at: new Date(), - pinned_by: client.user, + pinned_by: client.user as UserResponse | undefined, }; updateMessage(optimisticMessage); @@ -36,10 +36,10 @@ export const usePinHandler = (message: LocalMessage) => { try { const optimisticMessage = { ...message, - pin_expires: null, + pin_expires: undefined, pinned: false, - pinned_at: null, - pinned_by: null, + pinned_at: undefined, + pinned_by: undefined, }; updateMessage(optimisticMessage); diff --git a/src/components/Message/hooks/useReactionsFetcher.ts b/src/components/Message/hooks/useReactionsFetcher.ts index aac3d7c6eb..ed17fb6270 100644 --- a/src/components/Message/hooks/useReactionsFetcher.ts +++ b/src/components/Message/hooks/useReactionsFetcher.ts @@ -30,12 +30,13 @@ async function fetchMessageReactions( let hasNext = true; while (hasNext && reactions.length < MAX_MESSAGE_REACTIONS_TO_FETCH) { - const response = await client.queryReactions( - messageId, - reactionType ? { type: reactionType } : {}, + const response = await client.queryReactions({ + filter: reactionType ? { type: reactionType } : {}, + id: messageId, + limit, + next, sort, - { limit, next }, - ); + }); reactions.push(...response.reactions); next = response.next; diff --git a/src/components/Message/utils.tsx b/src/components/Message/utils.tsx index 2aa667576c..9f8b4e38f3 100644 --- a/src/components/Message/utils.tsx +++ b/src/components/Message/utils.tsx @@ -6,10 +6,9 @@ import type { TFunction } from 'i18next'; import type { ChannelConfigWithInfo, LocalMessage, - LocalMessageBase, MessageResponse, - Mute, StreamChat, + UserMuteResponse, UserResponse, } from 'stream-chat'; import type { MessageProps } from './types'; @@ -41,10 +40,10 @@ export const validateAndGetMessage = ( /** * Tell if the owner of the current message is muted */ -export const isUserMuted = (message: LocalMessage, mutes?: Mute[]) => { +export const isUserMuted = (message: LocalMessage, mutes?: UserMuteResponse[]) => { if (!mutes || !message) return false; - const userMuted = mutes.filter((el) => el.target.id === message.user?.id); + const userMuted = mutes.filter((el) => el.target?.id === message.user?.id); return !!userMuted.length; }; @@ -178,10 +177,7 @@ export const ACTIONS_NOT_WORKING_IN_THREAD = [ ]; function areMessagesEqual(prevMessage: LocalMessage, nextMessage: LocalMessage): boolean { - const areBaseMessagesEqual = ( - prevMessage: LocalMessageBase, - nextMessage: LocalMessageBase, - ) => + const areBaseMessagesEqual = (prevMessage: LocalMessage, nextMessage: LocalMessage) => prevMessage.deleted_at === nextMessage.deleted_at && prevMessage.latest_reactions?.length === nextMessage.latest_reactions?.length && prevMessage.own_reactions?.length === nextMessage.own_reactions?.length && @@ -199,19 +195,19 @@ function areMessagesEqual(prevMessage: LocalMessage, nextMessage: LocalMessage): Boolean(prevMessage.quoted_message) === Boolean(nextMessage.quoted_message) && ((!prevMessage.quoted_message && !nextMessage.quoted_message) || areBaseMessagesEqual( - prevMessage.quoted_message as LocalMessageBase, - nextMessage.quoted_message as LocalMessageBase, + prevMessage.quoted_message as LocalMessage, + nextMessage.quoted_message as LocalMessage, )) ); } export const areMessagePropsEqual = ( prevProps: MessageProps & { - mutes?: Mute[]; + mutes?: UserMuteResponse[]; showDetailedReactions?: boolean; }, nextProps: MessageProps & { - mutes?: Mute[]; + mutes?: UserMuteResponse[]; showDetailedReactions?: boolean; }, ) => { @@ -397,20 +393,14 @@ export const isMessageErrorRetryable = (message: LocalMessage) => export const isNetworkSendFailure = (message: Pick) => message.status === 'failed' && message.error?.status === 0; -export const isMessageBounced = ( - message: Pick, -) => - message.type === 'error' && - (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_BOUNCE' || - message.moderation?.action === 'bounce'); +export const isMessageBounced = (message: Pick) => + message.type === 'error' && message.moderation?.action === 'bounce'; export const isMessageBlocked = ( - message: Pick, + message: Pick, ) => message.shadowed || - (message.type === 'error' && - (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_REMOVE' || - message.moderation?.action === 'remove')); + (message.type === 'error' && message.moderation?.action === 'remove'); export const isMessageDeleted = (message: LocalMessage): boolean => Boolean(message.deleted_at || message.type === 'deleted' || message.deleted_for_me); diff --git a/src/components/MessageActions/DownloadSubmenu.tsx b/src/components/MessageActions/DownloadSubmenu.tsx index a512771c4a..63412c2cbd 100644 --- a/src/components/MessageActions/DownloadSubmenu.tsx +++ b/src/components/MessageActions/DownloadSubmenu.tsx @@ -8,6 +8,7 @@ import { } from '../Dialog'; import { IconChevronLeft, IconDownload } from '../Icons'; import { + type DownloadableAttachment, downloadAllAttachments, downloadAttachment, isDownloadableAttachment, @@ -34,9 +35,10 @@ export const DownloadSubmenu = () => { const { message } = useMessageContext(); const { t } = useTranslationContext(); + // TODO: something here is improperly typed const downloadableAttachments = (message.attachments ?? []).filter( isDownloadableAttachment, - ); + ) as DownloadableAttachment[]; return (
{ ); }; -export const useCommandTranslation = (command: CommandResponse) => { +export const useCommandTranslation = (command: Command) => { const { t } = useTranslationContext(); const knownArgsTranslations = useMemo>( @@ -124,7 +124,7 @@ export const CommandContextMenuItem = ({ enabled = true, ...props }: ComponentProps<'button'> & { - command: CommandResponse & { name: string }; + command: Command & { name: string }; enabled?: boolean; }) => { const { args, description } = useCommandTranslation(command); diff --git a/src/components/MessageComposer/QuotedMessagePreview.tsx b/src/components/MessageComposer/QuotedMessagePreview.tsx index d9cf3e345d..f9559c24ae 100644 --- a/src/components/MessageComposer/QuotedMessagePreview.tsx +++ b/src/components/MessageComposer/QuotedMessagePreview.tsx @@ -25,11 +25,11 @@ import { isVideoAttachment, isVoiceRecordingAttachment, type LocalMessage, - type LocalMessageBase, type MessageComposerState, - type PollResponse, - type SharedLocationResponse, + type PollResponseData, + type SharedLocationResponseData, type TranslationLanguages, + type VoiceRecordingAttachment, } from 'stream-chat'; import { useChannelStateContext } from '../../context/ChannelStateContext'; import type { MessageContextValue } from '../../context'; @@ -91,8 +91,8 @@ const getAttachmentType = (attachment: Attachment) => { type GroupedAttachments = Record & { giphies: Attachment[]; - locations: SharedLocationResponse[]; - polls: PollResponse[]; + locations: SharedLocationResponseData[]; + polls: PollResponseData[]; total: number; }; @@ -224,7 +224,10 @@ const getAttachmentIconWithType = ( ...result, Icon: IconFile, PreviewImage: ( - + ), previewType: 'file', }; @@ -311,7 +314,7 @@ export const QuotedMessagePreview = ({ }; type QuotedMessagePreviewUIProps = QuotedMessagePreviewProps & { - quotedMessage: LocalMessageBase; + quotedMessage: LocalMessage; authorLabel?: ReactNode; className?: string; onClick?: MouseEventHandler; @@ -378,8 +381,9 @@ export const QuotedMessagePreviewUI = ({ { const voiceRecording = groupedAttachments.voiceRecordings[0]; renderedText = t('Voice message {{ duration }}', { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - duration: displayDuration(voiceRecording!.duration), + duration: displayDuration( + (voiceRecording as VoiceRecordingAttachment).custom.duration, + ), }); } } else if (previewType === 'giphy') { diff --git a/src/components/MessageComposer/hooks/useMessageComposerCommands.ts b/src/components/MessageComposer/hooks/useMessageComposerCommands.ts index 94e7b41fe5..7040059cf3 100644 --- a/src/components/MessageComposer/hooks/useMessageComposerCommands.ts +++ b/src/components/MessageComposer/hooks/useMessageComposerCommands.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import type { CommandResponse, MessageComposerState } from 'stream-chat'; +import type { Command, MessageComposerState } from 'stream-chat'; import { useStateStore } from '../../../store'; import { useMessageComposerController } from './useMessageComposerController'; @@ -13,7 +13,7 @@ const messageComposerStateSelector = ({ }); export type MessageComposerCommand = { - command: CommandResponse & { name: string }; + command: Command & { name: string }; enabled: boolean; }; @@ -29,7 +29,7 @@ export const useMessageComposerCommands = () => { () => (channelConfig?.commands ?? []) .filter( - (command): command is CommandResponse & { name: string } => !!command.name, + (command): command is Command & { name: string } => !!command.name, ) .map((command) => ({ command, diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx index 4e64f5afa8..1330f7a865 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx @@ -7,12 +7,12 @@ import { usePollContext, useTranslationContext, } from '../../../../context'; -import type { PollOption, PollState, PollVote } from 'stream-chat'; +import type { PollOption, PollState, PollVoteResponseData } from 'stream-chat'; import { Button } from '../../../Button'; import clsx from 'clsx'; type PollStateSelectorReturnValue = { - latest_votes_by_option: Record; + latest_votes_by_option: Record; }; const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue => ({ diff --git a/src/components/Poll/PollOptionSelector.tsx b/src/components/Poll/PollOptionSelector.tsx index 5870186b28..79a576e962 100644 --- a/src/components/Poll/PollOptionSelector.tsx +++ b/src/components/Poll/PollOptionSelector.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import React, { useMemo } from 'react'; -import type { PollOption, PollState, PollVote } from 'stream-chat'; +import type { PollOption, PollState, PollVoteResponseData } from 'stream-chat'; import { isVoteAnswer } from 'stream-chat'; import { AvatarStack as DefaultAvatarStack } from '../Avatar'; import { @@ -34,9 +34,9 @@ export const AmountBar = ({ amount, className }: AmountBarProps) => ( type PollStateSelectorReturnValue = { is_closed: boolean | undefined; - latest_votes_by_option: Record; + latest_votes_by_option: Record; maxVotedOptionIds: string[]; - ownVotesByOptionId: Record; + ownVotesByOptionId: Record; vote_counts_by_option: Record; voting_visibility: string; }; @@ -96,7 +96,7 @@ export const PollOptionSelector = ({ const avatarDisplayInfo = useMemo( () => latest_votes_by_option?.[option.id] && - (latest_votes_by_option[option.id] as PollVote[]) + (latest_votes_by_option[option.id] as PollVoteResponseData[]) .filter((vote) => !!vote.user && !isVoteAnswer(vote)) .slice(0, displayAvatarCount) .map(({ user }) => ({ diff --git a/src/components/Poll/PollVote.tsx b/src/components/Poll/PollVote.tsx index 6fa1808c08..059d0e2a15 100644 --- a/src/components/Poll/PollVote.tsx +++ b/src/components/Poll/PollVote.tsx @@ -4,7 +4,7 @@ import { PopperTooltip } from '../Tooltip'; import { useEnterLeaveHandlers } from '../Tooltip/hooks'; import { useChatContext, useTranslationContext } from '../../context'; -import type { PollVote as PollVoteType } from 'stream-chat'; +import type { PollVoteResponseData as PollVoteType } from 'stream-chat'; const PollVoteTimestamp = ({ timestamp }: { timestamp: Date | string }) => { const { t } = useTranslationContext(); diff --git a/src/components/Poll/hooks/useManagePollVotesRealtime.ts b/src/components/Poll/hooks/useManagePollVotesRealtime.ts index 1bbbee79d1..e8fd57faed 100644 --- a/src/components/Poll/hooks/useManagePollVotesRealtime.ts +++ b/src/components/Poll/hooks/useManagePollVotesRealtime.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { isVoteAnswer } from 'stream-chat'; import { useChatContext } from '../../../context'; -import type { EventPayload, PollAnswer, PollVote } from 'stream-chat'; +import type { EventPayload, PollVoteResponseData } from 'stream-chat'; import type { CursorPaginatorStateStore } from '../../InfiniteScrollPaginator/hooks/useCursorPaginator'; -export function useManagePollVotesRealtime( +export function useManagePollVotesRealtime( managedVoteType: 'answer' | 'vote', cursorPaginatorState?: CursorPaginatorStateStore, optionId?: string, diff --git a/src/components/Poll/hooks/usePollAnswerPagination.ts b/src/components/Poll/hooks/usePollAnswerPagination.ts index f9564e5962..653ae6538a 100644 --- a/src/components/Poll/hooks/usePollAnswerPagination.ts +++ b/src/components/Poll/hooks/usePollAnswerPagination.ts @@ -8,10 +8,10 @@ import { useCursorPaginator } from '../../InfiniteScrollPaginator/hooks/useCurso import { usePollContext } from '../../../context'; import { useStateStore } from '../../../store'; -import type { PollAnswer, PollAnswersQueryParams, PollVote } from 'stream-chat'; +import type { PollAnswersQueryParams, PollVoteResponseData } from 'stream-chat'; const paginationStateSelector = ( - state: CursorPaginatorState, + state: CursorPaginatorState, ): [Error | undefined, boolean, boolean] => [ state.error, state.hasNextPage, @@ -27,14 +27,14 @@ export const usePollAnswerPagination = ({ }: UsePollAnswerPaginationParams = {}) => { const { poll } = usePollContext(); - const paginationFn = useCallback>( + const paginationFn = useCallback>( async (next) => { const { next: newNext, votes } = await poll.queryAnswers({ filter: paginationParams?.filter, options: !next ? paginationParams?.options : { ...paginationParams?.options, next }, - sort: { created_at: -1, ...paginationParams?.sort }, + sort: [{ direction: -1, field: 'created_at' }, ...(paginationParams?.sort ?? [])], }); return { items: votes, next: newNext }; }, @@ -42,7 +42,10 @@ export const usePollAnswerPagination = ({ ); const { cursorPaginatorState, loadMore } = useCursorPaginator(paginationFn, true); - const answers = useManagePollVotesRealtime('answer', cursorPaginatorState); + const answers = useManagePollVotesRealtime( + 'answer', + cursorPaginatorState, + ); const [error, hasNextPage, loading] = useStateStore( cursorPaginatorState, paginationStateSelector, diff --git a/src/components/Poll/hooks/usePollOptionVotesPagination.ts b/src/components/Poll/hooks/usePollOptionVotesPagination.ts index 49052fdf04..6dd6632c83 100644 --- a/src/components/Poll/hooks/usePollOptionVotesPagination.ts +++ b/src/components/Poll/hooks/usePollOptionVotesPagination.ts @@ -8,10 +8,10 @@ import { useCursorPaginator } from '../../InfiniteScrollPaginator/hooks/useCurso import { useStateStore } from '../../../store'; import { usePollContext } from '../../../context'; -import type { PollOptionVotesQueryParams, PollVote } from 'stream-chat'; +import type { PollOptionVotesQueryParams, PollVoteResponseData } from 'stream-chat'; const paginationStateSelector = ( - state: CursorPaginatorState, + state: CursorPaginatorState, ): [Error | undefined, boolean, boolean] => [ state.error, state.hasNextPage, @@ -27,14 +27,14 @@ export const usePollOptionVotesPagination = ({ }: UsePollOptionVotesPaginationParams) => { const { poll } = usePollContext(); - const paginationFn = useCallback>( + const paginationFn = useCallback>( async (next) => { const { next: newNext, votes } = await poll.queryOptionVotes({ filter: paginationParams.filter, options: !next ? paginationParams?.options : { ...paginationParams?.options, next }, - sort: { created_at: -1, ...paginationParams?.sort }, + sort: [{ direction: -1, field: 'created_at' }, ...(paginationParams?.sort ?? [])], }); return { items: votes, next: newNext }; }, @@ -42,7 +42,7 @@ export const usePollOptionVotesPagination = ({ ); const { cursorPaginatorState, loadMore } = useCursorPaginator(paginationFn, true); - const votes = useManagePollVotesRealtime( + const votes = useManagePollVotesRealtime( 'vote', cursorPaginatorState, paginationParams.filter.option_id, diff --git a/src/components/Search/SearchResults/SearchResultItem.tsx b/src/components/Search/SearchResults/SearchResultItem.tsx index 87f28ed893..7527cb04ed 100644 --- a/src/components/Search/SearchResults/SearchResultItem.tsx +++ b/src/components/Search/SearchResults/SearchResultItem.tsx @@ -1,7 +1,14 @@ import React, { useCallback, useMemo } from 'react'; import uniqBy from 'lodash.uniqby'; import type { ComponentType } from 'react'; -import type { Channel, MessageResponse, User } from 'stream-chat'; +import type { + Channel, + ChannelResponse, + MessageResponse, + UserResponse, +} from 'stream-chat'; + +type SearchResultMessage = MessageResponse & { channel?: ChannelResponse }; import { useSearchContext } from '../SearchContext'; import { Avatar } from '../../../components/Avatar'; @@ -37,7 +44,7 @@ export const ChannelSearchResultItem = ({ item }: ChannelSearchResultItemProps) }; export type ChannelByMessageSearchResultItemProps = { - item: MessageResponse; + item: SearchResultMessage; }; export const MessageSearchResultItem = ({ @@ -91,7 +98,7 @@ export const MessageSearchResultItem = ({ }; export type UserSearchResultItemProps = { - item: User; + item: UserResponse; }; export const UserSearchResultItem = ({ item }: UserSearchResultItemProps) => { @@ -102,7 +109,7 @@ export const UserSearchResultItem = ({ item }: UserSearchResultItemProps) => { const onClick = useCallback(() => { const newChannel = client.channel(directMessagingChannelType, { - members: [client.userID as string, item.id], + members: [{ user_id: client.userId as string }, { user_id: item.id }], }); newChannel.watch(); setActiveChannel(newChannel); @@ -128,7 +135,8 @@ export const UserSearchResultItem = ({ item }: UserSearchResultItemProps) => { />
- {item.name || item.username || item.id} + {/* @ts-expect-error username is not typed */} + {item.name || item.custom.username || item.id}
; @@ -29,12 +29,12 @@ export const CommandItem = (props: PropsWithChildren) => { const resolvedEnabled = enabled ?? - !messageComposer.isCommandDisabled(entity as CommandResponse & { name: string }); + !messageComposer.isCommandDisabled(entity as Command & { name: string }); return ( ); diff --git a/src/context/ChannelStateContext.tsx b/src/context/ChannelStateContext.tsx index 3aeb8ab128..dbbc296c7e 100644 --- a/src/context/ChannelStateContext.tsx +++ b/src/context/ChannelStateContext.tsx @@ -5,8 +5,8 @@ import type { ChannelConfigWithInfo, GiphyVersions, LocalMessage, - Mute, ChannelState as StreamChannelState, + UserMuteResponse, } from 'stream-chat'; import type { @@ -55,7 +55,7 @@ export type ChannelStateContextValue = Omit & { videoAttachmentSizeHandler: VideoAttachmentSizeHandler; channelUnreadUiState?: ChannelUnreadUiState; giphyVersion?: GiphyVersions; - mutes?: Array; + mutes?: Array; watcher_count?: number; }; diff --git a/src/context/ChatContext.tsx b/src/context/ChatContext.tsx index b10ef1b364..abbebe04e5 100644 --- a/src/context/ChatContext.tsx +++ b/src/context/ChatContext.tsx @@ -1,6 +1,11 @@ import React, { useContext } from 'react'; import type { PropsWithChildren } from 'react'; -import type { Channel, Mute, SearchController, StreamChat } from 'stream-chat'; +import type { + Channel, + SearchController, + StreamChat, + UserMuteResponse, +} from 'stream-chat'; import type { ChatProps } from '../components/Chat/Chat'; import type { ChannelsQueryState } from '../components/Chat/hooks/useChannelsQueryState'; @@ -28,7 +33,7 @@ export type ChatContextValue = { channelsQueryState: ChannelsQueryState; getAppSettings: () => ReturnType | null; latestMessageDatesByChannels: Record; - mutes: Array; + mutes: Array; /** Instance of SearchController class that allows to control all the search operations. */ searchController: SearchController; /** diff --git a/src/context/MessageContext.tsx b/src/context/MessageContext.tsx index a16a8920ef..f411c08820 100644 --- a/src/context/MessageContext.tsx +++ b/src/context/MessageContext.tsx @@ -4,9 +4,9 @@ import React, { useContext } from 'react'; import type { DeleteMessageOptions, LocalMessage, - Mute, ReactionResponse, ReactionSort, + UserMuteResponse, UserResponse, } from 'stream-chat'; @@ -99,7 +99,7 @@ export type MessageContextValue = { /** DOMRect object for parent MessageList component */ messageListRect?: DOMRect; /** Array of muted users coming from [ChannelStateContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_state_context/#mutes) */ - mutes?: Mute[]; + mutes?: UserMuteResponse[]; /** Sort options to provide to a reactions query */ reactionDetailsSort?: ReactionSort; /** A list of users that have read this Message */ diff --git a/src/utils/getChannel.ts b/src/utils/getChannel.ts index d88cfc9b06..6e17512dc0 100644 --- a/src/utils/getChannel.ts +++ b/src/utils/getChannel.ts @@ -1,7 +1,7 @@ import type { Channel, ChannelQueryOptions, - QueryChannelAPIResponse, + ChannelStateResponse, StreamChat, } from 'stream-chat'; @@ -11,7 +11,7 @@ import type { */ const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record< string, - Promise | undefined + Promise | undefined > = {}; type GetChannelParams = { From 4ce518cc464cf08466c8a2e83b0d84b2acc0bc5d Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 8 Jul 2026 12:40:53 +0200 Subject: [PATCH 3/5] Type renames --- src/components/Channel/Channel.tsx | 12 ++++++------ src/components/Message/hooks/useReactionHandler.ts | 4 ++-- src/components/MessageComposer/MessageComposer.tsx | 4 ++-- .../PollActions/PollResults/PollOptionWithVotes.tsx | 8 ++++++-- .../PollResults/PollOptionWithVotesHeader.tsx | 4 ++-- .../PollResults/PollOptionWithVotesList.tsx | 4 ++-- .../Poll/PollActions/PollResults/PollResults.tsx | 4 ++-- .../Poll/PollActions/SuggestPollOptionPrompt.tsx | 4 ++-- src/components/Poll/PollHeader.tsx | 4 ++-- src/components/Poll/PollOptionList.tsx | 4 ++-- src/components/Poll/PollOptionSelector.tsx | 4 ++-- src/context/ChannelActionContext.tsx | 4 ++-- src/utils/getChannel.ts | 4 ++-- 13 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index d3ad62347b..3cfd03e868 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -12,8 +12,8 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import throttle from 'lodash.throttle'; import type { + ChannelGetOrCreateRequest, ChannelMemberResponse, - ChannelQueryOptions, ChannelState, ChannelStateResponseFields, DeleteMessageOptions, @@ -21,7 +21,7 @@ import type { EventAPIResponse, GiphyVersions, LocalMessage, - Message, + MessageRequest, MessageResponse, SendMessageAPIResponse, SendMessageOptions, @@ -102,7 +102,7 @@ export type ChannelProps = { * If the channel instance has already been initialized (channel has been queried), * then the channel query will be skipped and channelQueryOptions will not be applied. */ - channelQueryOptions?: ChannelQueryOptions; + channelQueryOptions?: ChannelGetOrCreateRequest; /** Custom action handler to override the default `client.deleteMessage(message.id)` function */ doDeleteMessageRequest?: ( message: LocalMessage, @@ -116,7 +116,7 @@ export type ChannelProps = { /** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */ doSendMessageRequest?: ( channel: StreamChannel, - message: Message, + message: MessageRequest, options?: SendMessageOptions, ) => ReturnType | void; /** Custom action handler to override the default `client.updateMessage` request function (advanced usage only) */ @@ -905,7 +905,7 @@ const ChannelInner = ( options, }: { localMessage: LocalMessage; - message: Message; + message: MessageRequest; options?: SendMessageOptions; }) => { try { @@ -987,7 +987,7 @@ const ChannelInner = ( options, }: { localMessage: LocalMessage; - message: Message; + message: MessageRequest; options?: SendMessageOptions; }) => { channel.state.filterErrorMessages(); diff --git a/src/components/Message/hooks/useReactionHandler.ts b/src/components/Message/hooks/useReactionHandler.ts index 478af4d645..8669d0eb86 100644 --- a/src/components/Message/hooks/useReactionHandler.ts +++ b/src/components/Message/hooks/useReactionHandler.ts @@ -12,7 +12,7 @@ import { getEmojiCodeByReactionType, } from '../../Reactions/reactionOptions'; -import type { LocalMessage, Reaction, ReactionResponse } from 'stream-chat'; +import type { LocalMessage, ReactionRequest, ReactionResponse } from 'stream-chat'; export const reactionHandlerWarning = `Reaction handler was called, but it is missing one of its required arguments. Make sure the ChannelAction and ChannelState contexts are properly set and the hook is initialized with a valid message.`; @@ -105,7 +105,7 @@ export const useReactionHandler = (message?: LocalMessage) => { reaction: { type, ...(emojiCode && { emoji_code: emojiCode }), - } as Reaction, + } as ReactionRequest, }) : await channel.deleteReaction({ id, type }); diff --git a/src/components/MessageComposer/MessageComposer.tsx b/src/components/MessageComposer/MessageComposer.tsx index 70888f629a..4238e1747e 100644 --- a/src/components/MessageComposer/MessageComposer.tsx +++ b/src/components/MessageComposer/MessageComposer.tsx @@ -11,7 +11,7 @@ import { MessageComposerContextProvider } from '../../context/MessageComposerCon import { DialogManagerProvider } from '../../context'; import { useStableId } from '../UtilityComponents/useStableId'; -import type { LocalMessage, Message, SendMessageOptions } from 'stream-chat'; +import type { LocalMessage, MessageRequest, SendMessageOptions } from 'stream-chat'; import type { CustomAudioRecordingConfig } from '../MediaRecorder'; import { useRegisterDropHandlers } from './WithDragAndDropUpload'; @@ -64,7 +64,7 @@ export type MessageComposerProps = { overrideSubmitHandler?: (params: { cid: string; localMessage: LocalMessage; - message: Message; + message: MessageRequest; sendOptions: SendMessageOptions; }) => Promise | void; /** When replying in a thread, the parent message object */ diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx index 1330f7a865..0ea149bc29 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx @@ -7,7 +7,11 @@ import { usePollContext, useTranslationContext, } from '../../../../context'; -import type { PollOption, PollState, PollVoteResponseData } from 'stream-chat'; +import type { + PollOptionResponseData, + PollState, + PollVoteResponseData, +} from 'stream-chat'; import { Button } from '../../../Button'; import clsx from 'clsx'; @@ -20,7 +24,7 @@ const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue = }); export type PollOptionWithVotesProps = { - option: PollOption; + option: PollOptionResponseData; orderNumber: number; countVotesPreview?: number; showAllVotes?: () => void; diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx index 6e37c8bfc1..3aaf128d91 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useStateStore } from '../../../../store'; import { usePollContext, useTranslationContext } from '../../../../context'; -import type { PollOption, PollState } from 'stream-chat'; +import type { PollOptionResponseData, PollState } from 'stream-chat'; import { IconTrophy } from '../../../Icons'; type PollStateSelectorReturnValue = { @@ -40,7 +40,7 @@ export const PollResultOptionVoteCounter = ({ }; export type PollOptionWithVotesHeaderProps = { - option: PollOption; + option: PollOptionResponseData; optionOrderNumber: number; }; diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesList.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesList.tsx index e31f7fdd21..0be7453ade 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesList.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesList.tsx @@ -3,11 +3,11 @@ import { PollVoteListing } from '../../PollVote'; import { usePollOptionVotesPagination } from '../../hooks'; import { LoadingIndicator } from '../../../Loading'; import { InfiniteScrollPaginator } from '../../../InfiniteScrollPaginator/InfiniteScrollPaginator'; -import type { PollOption, PollOptionVotesQueryParams } from 'stream-chat'; +import type { PollOptionResponseData, PollOptionVotesQueryParams } from 'stream-chat'; import { PollOptionWithVotesHeader } from './PollOptionWithVotesHeader'; export type PollOptionWithVotesListProps = { - option: PollOption; + option: PollOptionResponseData; optionOrderNumber: number; }; diff --git a/src/components/Poll/PollActions/PollResults/PollResults.tsx b/src/components/Poll/PollActions/PollResults/PollResults.tsx index c1c00c437d..dbf45440a3 100644 --- a/src/components/Poll/PollActions/PollResults/PollResults.tsx +++ b/src/components/Poll/PollActions/PollResults/PollResults.tsx @@ -8,7 +8,7 @@ import { usePollContext, useTranslationContext, } from '../../../../context'; -import type { PollOption, PollState } from 'stream-chat'; +import type { PollOptionResponseData, PollState } from 'stream-chat'; import { COUNT_OPTION_VOTES_PREVIEW } from '../../constants'; import { PollQuestion } from '../PollQuestion'; import { PollOptionWithVotesList } from './PollOptionWithVotesList'; @@ -34,7 +34,7 @@ export const PollResults = () => { pollStateSelector, ); const [optionToView, setOptionToView] = useState<{ - option: PollOption; + option: PollOptionResponseData; optionOrderNumber: number; } | null>(null); diff --git a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx index bf76bb614e..4dbb26fae7 100644 --- a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx +++ b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx @@ -6,12 +6,12 @@ import { useTranslationContext, } from '../../../context'; import { useStateStore } from '../../../store'; -import type { PollOption, PollState } from 'stream-chat'; +import type { PollOptionResponseData, PollState } from 'stream-chat'; import { Prompt } from '../../Dialog'; import { TextInput } from '../../Form'; import { useFormState } from '../../Form/hooks'; -type PollStateSelectorReturnValue = { options: PollOption[] }; +type PollStateSelectorReturnValue = { options: PollOptionResponseData[] }; const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue => ({ options: nextValue.options, }); diff --git a/src/components/Poll/PollHeader.tsx b/src/components/Poll/PollHeader.tsx index 1ecca60a82..d692dd680a 100644 --- a/src/components/Poll/PollHeader.tsx +++ b/src/components/Poll/PollHeader.tsx @@ -1,14 +1,14 @@ import React, { useMemo } from 'react'; import { usePollContext, useTranslationContext } from '../../context'; import { useStateStore } from '../../store'; -import type { PollOption, PollState } from 'stream-chat'; +import type { PollOptionResponseData, PollState } from 'stream-chat'; type PollStateSelectorReturnValue = { enforce_unique_vote: boolean; is_closed: boolean | undefined; max_votes_allowed: number | undefined; name: string; - options: PollOption[]; + options: PollOptionResponseData[]; }; const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue => ({ enforce_unique_vote: nextValue.enforce_unique_vote, diff --git a/src/components/Poll/PollOptionList.tsx b/src/components/Poll/PollOptionList.tsx index aff7bee1b6..502750146d 100644 --- a/src/components/Poll/PollOptionList.tsx +++ b/src/components/Poll/PollOptionList.tsx @@ -8,10 +8,10 @@ import { usePollContext, useTranslationContext, } from '../../context'; -import type { PollOption, PollState } from 'stream-chat'; +import type { PollOptionResponseData, PollState } from 'stream-chat'; import { PollOptionsFullList as DefaultPollOptionsFullList } from './PollActions/PollOptionsFullList'; -type PollStateSelectorReturnValue = { options: PollOption[] }; +type PollStateSelectorReturnValue = { options: PollOptionResponseData[] }; const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue => ({ options: nextValue.options, diff --git a/src/components/Poll/PollOptionSelector.tsx b/src/components/Poll/PollOptionSelector.tsx index 79a576e962..eb7607022d 100644 --- a/src/components/Poll/PollOptionSelector.tsx +++ b/src/components/Poll/PollOptionSelector.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import React, { useMemo } from 'react'; -import type { PollOption, PollState, PollVoteResponseData } from 'stream-chat'; +import type { PollOptionResponseData, PollState, PollVoteResponseData } from 'stream-chat'; import { isVoteAnswer } from 'stream-chat'; import { AvatarStack as DefaultAvatarStack } from '../Avatar'; import { @@ -50,7 +50,7 @@ const pollStateSelector = (nextValue: PollState): PollStateSelectorReturnValue = }); export type PollOptionSelectorProps = { - option: PollOption; + option: PollOptionResponseData; displayAvatarCount?: number; voteCountVerbose?: boolean; }; diff --git a/src/context/ChannelActionContext.tsx b/src/context/ChannelActionContext.tsx index fd668fa7f1..62e4df673a 100644 --- a/src/context/ChannelActionContext.tsx +++ b/src/context/ChannelActionContext.tsx @@ -4,7 +4,7 @@ import React, { useContext } from 'react'; import type { DeleteMessageOptions, LocalMessage, - Message, + MessageRequest, MessageResponse, SendMessageOptions, UpdateMessageAPIResponse, @@ -59,7 +59,7 @@ export type ChannelActionContextValue = { retrySendMessage: RetrySendMessage; sendMessage: (params: { localMessage: LocalMessage; - message: Message; + message: MessageRequest; options?: SendMessageOptions; }) => Promise; setChannelUnreadUiState: React.Dispatch< diff --git a/src/utils/getChannel.ts b/src/utils/getChannel.ts index 6e17512dc0..ee718a0021 100644 --- a/src/utils/getChannel.ts +++ b/src/utils/getChannel.ts @@ -1,6 +1,6 @@ import type { Channel, - ChannelQueryOptions, + ChannelGetOrCreateRequest, ChannelStateResponse, StreamChat, } from 'stream-chat'; @@ -19,7 +19,7 @@ type GetChannelParams = { channel?: Channel; id?: string; members?: string[]; - options?: ChannelQueryOptions; + options?: ChannelGetOrCreateRequest; type?: string; }; /** From d39d5c679e84e3690678fcef6d07b5a0b1ae819a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 8 Jul 2026 12:41:09 +0200 Subject: [PATCH 4/5] Questionable changes --- .../Channel/hooks/useEditMessageHandler.ts | 28 ++++++++++++++----- .../MessageComposer/QuotedMessagePreview.tsx | 9 +++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/Channel/hooks/useEditMessageHandler.ts b/src/components/Channel/hooks/useEditMessageHandler.ts index 7605fc3395..52be114107 100644 --- a/src/components/Channel/hooks/useEditMessageHandler.ts +++ b/src/components/Channel/hooks/useEditMessageHandler.ts @@ -1,26 +1,40 @@ -import type { MessageRequest, StreamChat, UpdateMessageOptions } from 'stream-chat'; +import type { + LocalMessage, + MessageRequest, + MessageResponse, + StreamChat, + UpdateMessageAPIResponse, + UpdateMessageOptions, +} from 'stream-chat'; import { useChatContext } from '../../../context/ChatContext'; type UpdateHandler = ( cid: string, - updatedMessage: MessageRequest, + updatedMessage: LocalMessage | MessageResponse, options?: UpdateMessageOptions, ) => ReturnType; -export const useEditMessageHandler = (doUpdateMessageRequest?: UpdateHandler) => { +export const useEditMessageHandler = ( + doUpdateMessageRequest?: UpdateHandler, +): (( + updatedMessage: LocalMessage | MessageResponse, + options?: UpdateMessageOptions, +) => Promise) => { const { channel, client } = useChatContext('useEditMessageHandler'); - return (updatedMessage: MessageRequest, options?: UpdateMessageOptions) => { + return ( + updatedMessage: LocalMessage | MessageResponse, + options?: UpdateMessageOptions, + ) => { if (doUpdateMessageRequest && channel) { return Promise.resolve( doUpdateMessageRequest(channel.cid, updatedMessage, options), ); } return client.updateMessage({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - id: updatedMessage.id!, - message: updatedMessage, + id: updatedMessage.id, + message: updatedMessage as unknown as MessageRequest, ...options, }); }; diff --git a/src/components/MessageComposer/QuotedMessagePreview.tsx b/src/components/MessageComposer/QuotedMessagePreview.tsx index f9559c24ae..66b955f960 100644 --- a/src/components/MessageComposer/QuotedMessagePreview.tsx +++ b/src/components/MessageComposer/QuotedMessagePreview.tsx @@ -26,6 +26,7 @@ import { isVoiceRecordingAttachment, type LocalMessage, type MessageComposerState, + type MessageResponse, type PollResponseData, type SharedLocationResponseData, type TranslationLanguages, @@ -55,7 +56,7 @@ const messageComposerStateStoreSelector = (state: MessageComposerState) => ({ }); export type QuotedMessagePreviewProps = { - getQuotedMessageAuthor?: (message: LocalMessage) => string; + getQuotedMessageAuthor?: (message: LocalMessage | MessageResponse) => string; renderText?: MessageContextValue['renderText']; }; @@ -96,7 +97,7 @@ type GroupedAttachments = Record & { total: number; }; -const getGroupedAttachments = (quotedMessage: LocalMessage | null) => { +const getGroupedAttachments = (quotedMessage: LocalMessage | MessageResponse | null) => { const groupedAttachments = { documents: [], giphies: [], @@ -169,7 +170,7 @@ type PreviewType = | 'mixed'; const getAttachmentIconWithType = ( - quotedMessage: LocalMessage | null, + quotedMessage: LocalMessage | MessageResponse | null, giphyVersionName: GiphyVersions, ): { groupedAttachments: GroupedAttachments; @@ -314,7 +315,7 @@ export const QuotedMessagePreview = ({ }; type QuotedMessagePreviewUIProps = QuotedMessagePreviewProps & { - quotedMessage: LocalMessage; + quotedMessage: LocalMessage | MessageResponse; authorLabel?: ReactNode; className?: string; onClick?: MouseEventHandler; From 8bd2f18fa1072e4439caaed52f510bc30eed8593 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 8 Jul 2026 16:02:55 +0200 Subject: [PATCH 5/5] Migrate Vite example --- examples/vite/src/App.tsx | 7 +++- .../websocketEventAutomation.ts | 34 +++++++++-------- .../websocketEventTemplates.ts | 37 ++++++++++--------- .../tabs/Reactions/reactionsExampleData.ts | 24 ++++++++---- .../ChannelPreviewOverlay.tsx | 6 +-- .../ChatLayout/ChannelMembersRemoveView.tsx | 3 +- examples/vite/src/ChatLayout/Sync.tsx | 2 +- .../ConfigurableMessageActions.tsx | 2 +- .../vite/src/CustomMessageUi/variants.tsx | 2 +- .../PublicChannelOverlay.tsx | 6 +-- 10 files changed, 70 insertions(+), 53 deletions(-) diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 9ce215e2f7..47cbb12e95 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -97,7 +97,10 @@ if (!apiKey) { throw new Error('VITE_STREAM_API_KEY is not defined'); } -const sort: ChannelSort = { last_message_at: -1, updated_at: -1 }; +const sort: ChannelSort = [ + { field: 'last_message_at', direction: -1 }, + { field: 'updated_at', direction: -1 }, +]; // @ts-expect-error ai_generated isn't on LocalMessage's public type yet const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated; @@ -297,7 +300,7 @@ const App = () => { $or: { enabled: true, generate: () => ({ - $or: [{ members: { $in: [chatClient.userID!] } }, { type: 'public' }], + $or: [{ members: { $in: [chatClient.userId!] } }, { type: 'public' }], members: undefined, }), }, diff --git a/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventAutomation.ts b/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventAutomation.ts index 4c6577fed5..b05568f9df 100644 --- a/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventAutomation.ts +++ b/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventAutomation.ts @@ -2,7 +2,7 @@ import type { Channel, ChannelMemberResponse, Event, - MessageResponseBase, + MessageResponse, ReactionResponse, StreamChat, UserResponse, @@ -16,15 +16,13 @@ import { import type { SimulationState, SimulationUser } from './types'; type UnknownRecord = Record; -type EventPayload = Omit< - Partial, - 'channel' | 'member' | 'message' | 'reaction' | 'user' -> & { +type EventPayload = { channel?: Partial; member?: ChannelMemberResponse; - message?: Partial; + message?: Partial; reaction?: ReactionResponse; user?: UserResponse; + [key: string]: unknown; }; const messageTextFragments = [ @@ -86,7 +84,7 @@ const buildReactionState = ({ }: { reaction: ReactionResponse; }): Pick< - MessageResponseBase, + MessageResponse, 'latest_reactions' | 'reaction_counts' | 'reaction_groups' | 'reaction_scores' > => { const reactionType = getId(reaction.type) ?? 'love'; @@ -113,7 +111,10 @@ const buildReactionState = ({ reaction_scores: { [reactionType]: reactionScore, }, - }; + } as unknown as Pick< + MessageResponse, + 'latest_reactions' | 'reaction_counts' | 'reaction_groups' | 'reaction_scores' + >; }; const uniquePush = (items: T[], value: T) => { @@ -390,7 +391,7 @@ export const buildFreshWebSocketEventPayload = ({ text, updated_at: freshContext.createdAt, user, - }, + } as unknown as Partial, message_id: messageId, user: eventType === 'message.new' ? user : basePayload.user, }; @@ -407,13 +408,14 @@ export const buildFreshWebSocketEventPayload = ({ const reaction = { ...baseReaction, created_at: freshContext.createdAt, + custom: {}, message_id: messageId, type: reactionType, updated_at: freshContext.createdAt, user, user_id: user.id, score: reactionScore, - }; + } as unknown as ReactionResponse; return { ...basePayload, @@ -430,7 +432,7 @@ export const buildFreshWebSocketEventPayload = ({ updated_at: freshContext.createdAt, user, ...buildReactionState({ reaction }), - }, + } as unknown as Partial, message_id: messageId, reaction, user, @@ -464,7 +466,7 @@ export const buildFreshWebSocketEventPayload = ({ member, updated_at: freshContext.createdAt, user, - }, + } as unknown as Partial, user, }; } @@ -494,7 +496,7 @@ export const trackSimulationStateFromPayload = ({ simulationState, templateContext, }: { - payload: Event; + payload: EventPayload; simulationState: SimulationState; templateContext: WebSocketEventTemplateContext; }) => { @@ -555,12 +557,12 @@ export const emitWebSocketEventPayload = ({ simulationState: SimulationState; templateContext: WebSocketEventTemplateContext; }) => { - const emittedPayload = { + const emittedPayload: EventPayload = { ...payload, type: eventType, - } as Event; + }; - client.dispatchEvent(emittedPayload); + client.dispatchEvent(emittedPayload as Event); trackSimulationStateFromPayload({ payload: emittedPayload, diff --git a/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventTemplates.ts b/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventTemplates.ts index c642fc7b9b..150e13f9d9 100644 --- a/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventTemplates.ts +++ b/examples/vite/src/AppSettings/ActionsMenu/WebSocketEventPromptDialog/websocketEventTemplates.ts @@ -120,21 +120,22 @@ type BuildChannelSeedContext = Omit & channel: Partial; }; -const createFallbackUser = (id: string, createdAt: string): DebugUserResponse => ({ - banned: false, - blocked_user_ids: [], - created_at: createdAt, - id, - invisible: false, - language: '', - last_active: createdAt, - name: id, - online: true, - role: 'user', - shadow_banned: false, - teams: [], - updated_at: createdAt, -}); +const createFallbackUser = (id: string, createdAt: string): DebugUserResponse => + ({ + banned: false, + blocked_user_ids: [], + created_at: createdAt, + id, + invisible: false, + language: '', + last_active: createdAt, + name: id, + online: true, + role: 'user', + shadow_banned: false, + teams: [], + updated_at: createdAt, + }) as unknown as DebugUserResponse; const getUserId = (user: DebugUserResponse) => typeof user.id === 'string' ? user.id : 'debug-user'; @@ -154,7 +155,7 @@ const createMember = (user: DebugUserResponse): ChannelMemberResponse => { updated_at: createdAt, user, user_id: getUserId(user), - }; + } as unknown as ChannelMemberResponse; }; const getMemberUserId = (member: { user?: unknown; user_id?: unknown }) => { @@ -176,7 +177,7 @@ const buildChannel = ( ): DebugChannelResponse => { const createdAt = context.createdAt; - return { + return ({ cid: context.cid, config: { automod: 'disabled', @@ -253,7 +254,7 @@ const buildChannel = ( type: context.channelType, updated_at: createdAt, ...overrides, - }; + }) as unknown as DebugChannelResponse; }; const buildMe = ( diff --git a/examples/vite/src/AppSettings/tabs/Reactions/reactionsExampleData.ts b/examples/vite/src/AppSettings/tabs/Reactions/reactionsExampleData.ts index b186e61d53..e73265bcce 100644 --- a/examples/vite/src/AppSettings/tabs/Reactions/reactionsExampleData.ts +++ b/examples/vite/src/AppSettings/tabs/Reactions/reactionsExampleData.ts @@ -5,13 +5,14 @@ const firstLikeReactionTimestamp = '2026-02-12T06:39:56.237389Z'; const secondLikeReactionTimestamp = '2026-02-12T06:39:52.237389Z'; const heartReactionTimestamp = '2026-02-12T06:35:58.021196Z'; -export const reactionsPreviewMessage: LocalMessage = { +export const reactionsPreviewMessage = { created_at: new Date('2026-02-12T06:34:40.000000Z'), - deleted_at: null, + deleted_at: undefined, id: 'settings-preview-message-id', latest_reactions: [ { created_at: fireReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'fire', @@ -26,6 +27,7 @@ export const reactionsPreviewMessage: LocalMessage = { }, { created_at: firstLikeReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'like', @@ -40,6 +42,7 @@ export const reactionsPreviewMessage: LocalMessage = { }, { created_at: secondLikeReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'like', @@ -54,6 +57,7 @@ export const reactionsPreviewMessage: LocalMessage = { }, { created_at: heartReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'heart', @@ -61,10 +65,11 @@ export const reactionsPreviewMessage: LocalMessage = { user: { id: 'test-user-2' }, user_id: 'test-user-2', }, - ] as LocalMessage['latest_reactions'], + ] as unknown as LocalMessage['latest_reactions'], own_reactions: [ { created_at: fireReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'fire', @@ -74,6 +79,7 @@ export const reactionsPreviewMessage: LocalMessage = { }, { created_at: firstLikeReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'like', @@ -83,6 +89,7 @@ export const reactionsPreviewMessage: LocalMessage = { }, { created_at: heartReactionTimestamp, + custom: {}, message_id: 'settings-preview-message-id', score: 1, type: 'heart', @@ -90,29 +97,32 @@ export const reactionsPreviewMessage: LocalMessage = { user: { id: 'test-user' }, user_id: 'test-user', }, - ] as LocalMessage['own_reactions'], - pinned_at: null, + ] as unknown as LocalMessage['own_reactions'], + pinned_at: undefined, reaction_counts: { fire: 1, heart: 1, like: 2 }, reaction_groups: { fire: { count: 1, first_reaction_at: fireReactionTimestamp, last_reaction_at: fireReactionTimestamp, + latest_reactions_by: [], sum_scores: 1, }, heart: { count: 1, first_reaction_at: heartReactionTimestamp, last_reaction_at: heartReactionTimestamp, + latest_reactions_by: [], sum_scores: 1, }, like: { count: 2, first_reaction_at: secondLikeReactionTimestamp, last_reaction_at: firstLikeReactionTimestamp, + latest_reactions_by: [], sum_scores: 2, }, - } as LocalMessage['reaction_groups'], + } as unknown as LocalMessage['reaction_groups'], reaction_scores: { fire: 1, heart: 1, like: 2 }, status: 'received', text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lectus nibh, rutrum in risus eget, dictum commodo dolor. Donec augue nisi, sollicitudin sed magna ut, tincidunt pretium lorem. ', @@ -123,7 +133,7 @@ export const reactionsPreviewMessage: LocalMessage = { image: 'https://getstream.io/random_svg/?id=preview-user&name=Preview+User', name: 'Preview User', }, -}; +} as unknown as LocalMessage; export const reactionsPreviewChannelState = { channel: { diff --git a/examples/vite/src/ChannelPreviewOverlay/ChannelPreviewOverlay.tsx b/examples/vite/src/ChannelPreviewOverlay/ChannelPreviewOverlay.tsx index fcb1b52dcc..f4caf3c5ca 100644 --- a/examples/vite/src/ChannelPreviewOverlay/ChannelPreviewOverlay.tsx +++ b/examples/vite/src/ChannelPreviewOverlay/ChannelPreviewOverlay.tsx @@ -16,7 +16,7 @@ export const useChannelMembershipState = () => { const { client } = useChatContext(); const { channel } = useChannelStateContext(); const members = useChannelMembersState(channel); - const membership = members[client.userID!] as ChannelMemberResponse | undefined; + const membership = members[client.userId!] as ChannelMemberResponse | undefined; const isMember = typeof membership?.channel_role === 'string'; const canJoin = channel.data?.own_capabilities?.includes('join-channel'); @@ -32,7 +32,7 @@ export const ChannelPreviewOverlay = () => { const handleJoin = useCallback(async () => { setJoining(true); try { - await channel.addMembers([client.userID!]); + await channel.addMembers([client.userId!]); } catch (error) { addNotification({ emitter: 'ChannelPreviewOverlay', @@ -48,7 +48,7 @@ export const ChannelPreviewOverlay = () => { } finally { setJoining(false); } - }, [addNotification, channel, client.userID]); + }, [addNotification, channel, client.userId]); if (isMember) return null; diff --git a/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx b/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx index b701e44273..799c1821f7 100644 --- a/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx +++ b/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx @@ -26,7 +26,8 @@ const toError = (error: unknown) => error instanceof Error ? error : new Error('An unknown error occurred'); const getUserDisplayName = (user?: UserResponse) => - user?.name || user?.username || user?.id || ''; + // @ts-expect-error username is not typed + user?.name || user?.custom.username || user?.id || ''; const getMemberDisplayName = (member: ChannelMemberResponse) => getUserDisplayName(member.user) || member.user_id || ''; diff --git a/examples/vite/src/ChatLayout/Sync.tsx b/examples/vite/src/ChatLayout/Sync.tsx index eddc107cf6..3e716ed15f 100644 --- a/examples/vite/src/ChatLayout/Sync.tsx +++ b/examples/vite/src/ChatLayout/Sync.tsx @@ -198,7 +198,7 @@ export const ThreadStateSync = () => { isRestoringThread.current = true; client - .getThread(threadIdToRestore) + .getThreadAndHydrate(threadIdToRestore) .then((thread) => { if (!thread || cancelled) return; diff --git a/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx b/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx index 4f4598d67a..ee3a11ee5e 100644 --- a/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx +++ b/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx @@ -116,7 +116,7 @@ const CustomDeleteMessageAlert = ({ appearance='outline' className='str-chat__delete-message-alert__delete-button' data-testid='delete-message-alert-delete-button' - onClick={() => onDelete({ deleteForMe, hardDelete })} + onClick={() => onDelete({ delete_for_me: deleteForMe, hard: hardDelete })} size='md' variant='danger' > diff --git a/examples/vite/src/CustomMessageUi/variants.tsx b/examples/vite/src/CustomMessageUi/variants.tsx index e0e8866d79..a955290f72 100644 --- a/examples/vite/src/CustomMessageUi/variants.tsx +++ b/examples/vite/src/CustomMessageUi/variants.tsx @@ -183,7 +183,7 @@ const CustomMessageUiMetadata = ({ {messageTextUpdatedAt && (
Edited
diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx index 0de469fae7..bf3e22046a 100644 --- a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx @@ -16,7 +16,7 @@ export const usePublicChannelState = () => { const { client } = useChatContext(); const { channel } = useChannelStateContext(); const members = useChannelMembersState(channel); - const membership = members[client.userID!] as ChannelMemberResponse | undefined; + const membership = members[client.userId!] as ChannelMemberResponse | undefined; const isMember = typeof membership?.channel_role === 'string'; const canJoin = channel.data?.own_capabilities?.includes('join-channel'); @@ -32,7 +32,7 @@ export const PublicChannelOverlay = () => { const handleJoin = useCallback(async () => { setJoining(true); try { - await channel.addMembers([client.userID!]); + await channel.addMembers([client.userId!]); } catch (error) { addNotification({ emitter: 'PublicChannelOverlay', @@ -48,7 +48,7 @@ export const PublicChannelOverlay = () => { } finally { setJoining(false); } - }, [addNotification, channel, client.userID]); + }, [addNotification, channel, client.userId]); if (isMember || !canJoin) return null;