diff --git a/src/hooks/VoicePlayer/index.tsx b/src/hooks/VoicePlayer/index.tsx index 1047db4d9..61b7705d5 100644 --- a/src/hooks/VoicePlayer/index.tsx +++ b/src/hooks/VoicePlayer/index.tsx @@ -73,20 +73,23 @@ export const VoicePlayerProvider = ({ } }; - const pause = (groupKey: string | null) => { - if (currentGroupKey === groupKey && currentPlayer !== null) { - logger.info('VoicePlayer: Pause playing(by group key).'); - currentPlayer?.pause(); - } - if (groupKey === ALL) { - logger.info('VoicePlayer: Pause playing(all).'); - currentPlayer?.pause(); + const pause = (groupKey?: string) => { + if (currentPlayer) { + if (groupKey === currentGroupKey) { + logger.info('VoicePlayer: Pause playing(by group key).'); + currentPlayer.pause(); + } else if (groupKey === ALL) { + logger.info('VoicePlayer: Pause playing(all).'); + currentPlayer.pause(); + } + } else { + logger.warning('VoicePlayer: No currentPlayer to pause.'); } }; const play = ({ groupKey, - audioFile = null, + audioFile, audioFileUrl = '', }: VoicePlayerPlayProps): void => { if (groupKey !== currentGroupKey) { @@ -96,7 +99,7 @@ export const VoicePlayerProvider = ({ // Clear the previous AudioPlayer element const voicePlayerRoot = document.getElementById(VOICE_PLAYER_ROOT_ID); const voicePlayerAudioElement = document.getElementById(VOICE_PLAYER_AUDIO_ID); - if (voicePlayerAudioElement) { + if (voicePlayerRoot && voicePlayerAudioElement) { voicePlayerRoot.removeChild(voicePlayerAudioElement); } diff --git a/src/hooks/VoicePlayer/useVoicePlayer.tsx b/src/hooks/VoicePlayer/useVoicePlayer.tsx index 77b9151d5..98f33dbd5 100644 --- a/src/hooks/VoicePlayer/useVoicePlayer.tsx +++ b/src/hooks/VoicePlayer/useVoicePlayer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useVoicePlayerContext } from '.'; import { VOICE_PLAYER_AUDIO_ID } from '../../utils/consts'; import { useVoiceRecorderContext } from '../VoiceRecorder'; @@ -7,8 +7,8 @@ import { AudioUnitDefaultValue, VoicePlayerStatusType } from './dux/initialState import { generateGroupKey } from './utils'; export interface UseVoicePlayerProps { - key: string; - channelUrl: string; + key?: string; + channelUrl?: string; audioFile?: File; audioFileUrl?: string; } @@ -25,10 +25,10 @@ export interface UseVoicePlayerContext { export const useVoicePlayer = ({ key = '', channelUrl = '', - audioFile = null, + audioFile, audioFileUrl = '', }: UseVoicePlayerProps): UseVoicePlayerContext => { - const [groupKey] = useState(generateGroupKey(channelUrl, key)); + const groupKey = generateGroupKey(channelUrl, key); const { play, pause, diff --git a/src/hooks/VoiceRecorder/WebAudioUtils.ts b/src/hooks/VoiceRecorder/WebAudioUtils.ts index 8f23c4f26..3b1f228a3 100644 --- a/src/hooks/VoiceRecorder/WebAudioUtils.ts +++ b/src/hooks/VoiceRecorder/WebAudioUtils.ts @@ -1,8 +1,9 @@ // Thanks to https://codesandbox.io/s/media-recorder-api-downsampling-16k-mp3-encode-using-lame-js-forked-n1pblw import { VOICE_RECORDER_AUDIO_SAMPLE_RATE } from '../../utils/consts'; +// @ts-ignore import { WavHeader, Mp3Encoder } from '../../_externals/lamejs/lame.all'; -function encodeMp3(arrayBuffer: ArrayBuffer): WavHeader { +function encodeMp3(arrayBuffer: ArrayBuffer) { const wav = WavHeader.readHeader(new DataView(arrayBuffer)); const dataView = new Int16Array(arrayBuffer, wav.dataOffset, wav.dataLen / 2); const mp3Encoder = new Mp3Encoder(wav.channels, wav.sampleRate, 128); @@ -14,11 +15,13 @@ function encodeMp3(arrayBuffer: ArrayBuffer): WavHeader { if (wav.channels > 1) { for (let j = 0; j < samplesLeft.length; j++) { samplesLeft[j] = dataView[j * 2]; - samplesRight[j] = dataView[j * 2 + 1]; + if (samplesRight) { + samplesRight[j] = dataView[j * 2 + 1]; + } } } - const dataBuffer = []; + const dataBuffer: Int8Array[] = []; let remaining = samplesLeft.length; for (let i = 0; remaining >= maxSamples; i += maxSamples) { const left = samplesLeft.subarray(i, i + maxSamples); @@ -44,7 +47,7 @@ function downsampleToWav(file: File, callback: (buffer: ArrayBuffer) => void): v const fileReader = new FileReader(); fileReader.onload = function (ev) { // Decode audio - audioCtx.decodeAudioData(ev.target.result as ArrayBuffer, (buffer) => { + audioCtx.decodeAudioData(ev.target?.result as ArrayBuffer, (buffer) => { // this is where you down sample the audio, usually is 44100 samples per second const usingWebkit = !window.OfflineAudioContext; const offlineAudioCtx = new OfflineAudioContext(1, 16000 * buffer.duration, 16000); @@ -55,8 +58,8 @@ function downsampleToWav(file: File, callback: (buffer: ArrayBuffer) => void): v const reader = new FileReader(); reader.onload = function () { - const renderCompleteHandler = (evt): void => { - const renderedBuffer = usingWebkit ? evt.renderedBuffer : evt; + const renderCompleteHandler = (evt: OfflineAudioCompletionEvent | AudioBuffer) => { + const renderedBuffer = usingWebkit ? (evt as OfflineAudioCompletionEvent).renderedBuffer : (evt as AudioBuffer); const buffer = bufferToWav(renderedBuffer, renderedBuffer.length); if (callback) { callback(buffer); @@ -80,12 +83,12 @@ function downsampleToWav(file: File, callback: (buffer: ArrayBuffer) => void): v fileReader.readAsArrayBuffer(file); } -function bufferToWav(abuffer, len) { +function bufferToWav(abuffer: AudioBuffer, len: number) { const numOfChan = abuffer.numberOfChannels; const length = len * numOfChan * 2 + 44; const buffer = new ArrayBuffer(length); const view = new DataView(buffer); - const channels = []; + const channels: any[] = []; let i = 0; let sample; let offset = 0; @@ -105,9 +108,11 @@ function bufferToWav(abuffer, len) { setUint16(16); // 16-bit (hardcoded in this demo) setUint32(0x61746164); // "data" - chunk setUint32(length - pos - 4); // chunk length + // write interleaved data - for (i = 0; i < abuffer.numberOfChannels; i++) + for (i = 0; i < abuffer.numberOfChannels; i++) { channels.push(abuffer.getChannelData(i)); + } while (pos < length) { for (i = 0; i < numOfChan; i++) { @@ -122,15 +127,20 @@ function bufferToWav(abuffer, len) { return buffer; - function setUint16(data) { + function setUint16(data: number) { view.setUint16(pos, data, true); pos += 2; } - function setUint32(data) { + function setUint32(data: number) { view.setUint32(pos, data, true); pos += 4; } } +export interface WebAudioUtils { + downsampleToWav: typeof downsampleToWav; + encodeMp3: typeof encodeMp3; +} + export { downsampleToWav, encodeMp3 }; diff --git a/src/hooks/VoiceRecorder/index.tsx b/src/hooks/VoiceRecorder/index.tsx index 8eb901452..84681b9ca 100644 --- a/src/hooks/VoiceRecorder/index.tsx +++ b/src/hooks/VoiceRecorder/index.tsx @@ -9,6 +9,8 @@ import { VOICE_RECORDER_AUDIO_BIT_RATE, } from '../../utils/consts'; import useSendbirdStateContext from '../useSendbirdStateContext'; +import { type WebAudioUtils } from './WebAudioUtils'; +import { noop } from '../../utils/utils'; // Input props of VoiceRecorder export interface VoiceRecorderProps { @@ -22,11 +24,11 @@ export interface VoiceRecorderEventHandler { // Output of VoiceRecorder export interface VoiceRecorderContext { - start: (eventHandler?: VoiceRecorderEventHandler) => void, - stop: () => void, + start: (eventHandler?: VoiceRecorderEventHandler) => void; + stop: () => void; isRecordable: boolean; } -const noop = () => { /* noop */ }; + const Context = createContext({ start: noop, stop: noop, @@ -37,13 +39,13 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle const { children } = props; const { config } = useSendbirdStateContext(); const { logger, groupChannel } = config; - const [mediaRecorder, setMediaRecorder] = useState(null); + const [mediaRecorder, setMediaRecorder] = useState(null); const [isRecordable, setIsRecordable] = useState(false); const [permissionWarning, setPermissionWarning] = useState(false); const { stringSet } = useLocalization(); const isVoiceMessageEnabled = groupChannel.enableVoiceMessage; - const [webAudioUtils, setWebAudioUtils] = useState(null); + const [webAudioUtils, setWebAudioUtils] = useState(null); const browserSupportMimeType = BROWSER_SUPPORT_MIME_TYPE_LIST.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''; if (isVoiceMessageEnabled && !browserSupportMimeType) { @@ -52,13 +54,11 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle useEffect(() => { if (isVoiceMessageEnabled && !webAudioUtils) { - import('./WebAudioUtils').then((data) => { - setWebAudioUtils(data); - }); + import('./WebAudioUtils').then((module) => setWebAudioUtils(module)); } }, [isVoiceMessageEnabled, webAudioUtils]); - const start = useCallback((eventHandler: VoiceRecorderEventHandler): void => { + const start = useCallback((eventHandler?: VoiceRecorderEventHandler): void => { if (isVoiceMessageEnabled && !webAudioUtils) { logger.error('VoiceRecorder: Recording audio processor is being loaded.'); return; @@ -95,7 +95,8 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle mimeType: browserSupportMimeType, audioBitsPerSecond: VOICE_RECORDER_AUDIO_BIT_RATE, }); - mediaRecorder.ondataavailable = (e) => { // when recording stops + // when recording stops + mediaRecorder.ondataavailable = (e) => { logger.info('VoiceRecorder: Succeeded getting an available data.', e.data); const audioFile = new File([e.data], VOICE_MESSAGE_FILE_NAME, { lastModified: new Date().getTime(), @@ -108,10 +109,11 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle lastModified: new Date().getTime(), type: VOICE_MESSAGE_MIME_TYPE, }); - eventHandler?.onRecordingEnded(convertedAudioFile); + eventHandler?.onRecordingEnded?.(convertedAudioFile); logger.info('VoiceRecorder: Succeeded converting audio file.', convertedAudioFile); }); - stream?.getAudioTracks?.().forEach?.(track => track?.stop()); + const tracks = stream.getAudioTracks(); + tracks.forEach((track) => track.stop()); setIsRecordable(false); }; mediaRecorder.onstart = eventHandler?.onRecordingStarted ?? noop; @@ -133,22 +135,13 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle }, [mediaRecorder]); return ( - + {children} - { - permissionWarning && ( - setPermissionWarning(false)} - > - <>{stringSet.VOICE_RECORDING_PERMISSION_DENIED} - - ) - } + {permissionWarning && ( + setPermissionWarning(false)}> + <>{stringSet.VOICE_RECORDING_PERMISSION_DENIED} + + )} ); }; diff --git a/src/hooks/VoiceRecorder/useVoiceRecorder.tsx b/src/hooks/VoiceRecorder/useVoiceRecorder.tsx index 730739deb..50533cabf 100644 --- a/src/hooks/VoiceRecorder/useVoiceRecorder.tsx +++ b/src/hooks/VoiceRecorder/useVoiceRecorder.tsx @@ -1,6 +1,7 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { VoiceRecorderEventHandler, useVoiceRecorderContext } from '.'; import useSendbirdStateContext from '../useSendbirdStateContext'; +import { noop } from '../../utils/utils'; // export interface UseVoiceRecorderProps extends VoiceRecorderEventHandler { // /** @@ -22,23 +23,21 @@ export interface UseVoiceRecorderContext { cancel: () => void; recordingLimit: number; recordingTime: number; - recordedFile: File; + recordedFile: File | null; recordingStatus: VoiceRecorderStatus; } -const noop = () => { /* noop */ }; - export const useVoiceRecorder = ({ onRecordingStarted = noop, onRecordingEnded = noop, }: VoiceRecorderEventHandler): UseVoiceRecorderContext => { const { config } = useSendbirdStateContext(); const { voiceRecord } = config; - const { maxRecordingTime } = voiceRecord; + const maxRecordingTime = voiceRecord.maxRecordingTime; const voiceRecorder = useVoiceRecorderContext(); const { isRecordable } = voiceRecorder; - const [recordedFile, setRecordedFile] = useState(null); + const [recordedFile, setRecordedFile] = useState(null); const [recordingStatus, setRecordingStatus] = useState(VoiceRecorderStatus.PREPARING); useEffect(() => { if (isRecordable && recordingStatus === VoiceRecorderStatus.PREPARING) { @@ -72,11 +71,12 @@ export const useVoiceRecorder = ({ // Timer const [recordingTime, setRecordingTime] = useState(0); - let timer: ReturnType = null; + const timer = useRef | null>(null); function startTimer() { stopTimer(); setRecordingTime(0); - const interval = setInterval(() => { + + timer.current = setInterval(() => { setRecordingTime(prevTime => { const newTime = prevTime + 100; if (newTime > maxRecordingTime) { @@ -85,11 +85,12 @@ export const useVoiceRecorder = ({ return newTime; }); }, 100); - timer = interval; } function stopTimer() { - clearInterval(timer); - timer = null; + if (timer.current) { + clearInterval(timer.current); + timer.current = null; + } } useEffect(() => { if (recordingTime > maxRecordingTime) { diff --git a/src/hooks/useThrottleCallback.ts b/src/hooks/useThrottleCallback.ts index b9fed48a1..99db7b175 100644 --- a/src/hooks/useThrottleCallback.ts +++ b/src/hooks/useThrottleCallback.ts @@ -12,8 +12,8 @@ export function useThrottleCallback void>( trailing: false, }, ) { - const timer = useRef(null); - const trailingArgs = useRef(null); + const timer = useRef | null>(null); + const trailingArgs = useRef(null); useEffect(() => { return () => { @@ -58,8 +58,8 @@ export function throttle void>( trailing: false, }, ) { - let timer = null; - let trailingArgs = null; + let timer: ReturnType | null = null; + let trailingArgs: null | any[] = null; return ((...args: any[]) => { if (timer) { diff --git a/src/lib/LocalizationContext.tsx b/src/lib/LocalizationContext.tsx index 10857d790..ad269faf2 100644 --- a/src/lib/LocalizationContext.tsx +++ b/src/lib/LocalizationContext.tsx @@ -4,31 +4,22 @@ import getStringSet, { StringSet } from '../ui/Label/stringSet'; import type { Locale } from 'date-fns'; import en from 'date-fns/locale/en-US'; -const LocalizationContext = React.createContext({ +const LocalizationContextDefault = { stringSet: getStringSet('en'), dateLocale: en, -}); +}; +const LocalizationContext = React.createContext(LocalizationContextDefault); interface LocalizationProviderProps { stringSet: StringSet; - dateLocale: Locale; + dateLocale?: Locale; children: React.ReactElement; } const LocalizationProvider = (props: LocalizationProviderProps): React.ReactElement => { const { children } = props; - return ( - - {children} - - ); + return {children}; }; -export type UseLocalizationType = () => { - stringSet: StringSet; - dateLocale: Locale; -}; - -const useLocalization: UseLocalizationType = () => React.useContext(LocalizationContext); - +const useLocalization = () => React.useContext(LocalizationContext); export { LocalizationContext, LocalizationProvider, useLocalization }; diff --git a/src/lib/Logger/index.ts b/src/lib/Logger/index.ts index 7a7eba3c6..f1c180c07 100644 --- a/src/lib/Logger/index.ts +++ b/src/lib/Logger/index.ts @@ -27,7 +27,7 @@ const colorLog = (level: LogLevel): string => { interface PrintLogProps { level: LogLevel; - title: string; + title?: string; description?: string; payload?: unknown[]; } diff --git a/src/lib/Sendbird.tsx b/src/lib/Sendbird.tsx index 386f3cc26..3756bdb3c 100644 --- a/src/lib/Sendbird.tsx +++ b/src/lib/Sendbird.tsx @@ -2,7 +2,6 @@ import './index.scss'; import './__experimental__typography.scss'; import React, { useEffect, useMemo, useReducer, useState } from 'react'; -import { User } from '@sendbird/chat'; import { GroupChannel } from '@sendbird/chat/groupChannel'; import { UIKitConfigProvider, useUIKitConfig } from '@sendbird/uikit-tools'; @@ -29,6 +28,7 @@ import { MediaQueryProvider, useMediaQueryContext } from './MediaQueryContext'; import getStringSet, { StringSet } from '../ui/Label/stringSet'; import { DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT, + DEFAULT_UPLOAD_SIZE_LIMIT, VOICE_RECORDER_DEFAULT_MAX, VOICE_RECORDER_DEFAULT_MIN, } from '../utils/consts'; @@ -45,10 +45,11 @@ import { CommonUIKitConfigProps, SendbirdChatInitParams, CustomExtensionParams, - SBUEventHandlers, SendbirdProviderUtils, + SBUEventHandlers, + SendbirdProviderUtils, } from './types'; import { GlobalModalProvider, ModalRoot } from '../hooks/useModal'; -import { RenderUserProfileProps } from '../types'; +import { RenderUserProfileProps, UserListQuery } from '../types'; import PUBSUB_TOPICS, { SBUGlobalPubSub, SBUGlobalPubSubTopicPayloadUnion } from './pubSub/topics'; import { EmojiManager } from './emojiManager'; import { uikitConfigStorage } from './utils/uikitConfigStorage'; @@ -57,12 +58,6 @@ import { EmojiReactionListRoot, MenuRoot } from '../ui/ContextMenu'; export { useSendbirdStateContext } from '../hooks/useSendbirdStateContext'; -export type UserListQueryType = { - hasNext?: boolean; - next: () => Promise>; - get isLoading(): boolean; -}; - interface VoiceRecordOptions { maxRecordingTime?: number; minRecordingTime?: number; @@ -101,7 +96,7 @@ export interface SendbirdProviderProps extends CommonUIKitConfigProps, React.Pro dateLocale?: Locale; profileUrl?: string; voiceRecord?: VoiceRecordOptions; - userListQuery?(): UserListQueryType; + userListQuery?: () => UserListQuery; imageCompression?: ImageCompressionOptions; allowProfileEdit?: boolean; disableMarkAsDelivered?: boolean; @@ -126,10 +121,8 @@ export function SendbirdProvider(props: SendbirdProviderProps) { isReactionEnabled: props.isReactionEnabled, disableUserProfile: props.disableUserProfile, isVoiceMessageEnabled: props.isVoiceMessageEnabled, - isTypingIndicatorEnabledOnChannelList: - props.isTypingIndicatorEnabledOnChannelList, - isMessageReceiptStatusEnabledOnChannelList: - props.isMessageReceiptStatusEnabledOnChannelList, + isTypingIndicatorEnabledOnChannelList: props.isTypingIndicatorEnabledOnChannelList, + isMessageReceiptStatusEnabledOnChannelList: props.isMessageReceiptStatusEnabledOnChannelList, showSearchIcon: props.showSearchIcon, }, uikitOptions: props.uikitOptions, @@ -161,21 +154,21 @@ const SendbirdSDK = ({ accessToken, customApiHost, customWebSocketHost, - configureSession = null, + configureSession, theme = 'light', config = {}, nickname = '', - colorSet = null, - stringSet = null, - dateLocale = null, + colorSet, + stringSet, + dateLocale, profileUrl = '', voiceRecord, - userListQuery = null, + userListQuery, imageCompression = {}, allowProfileEdit = false, disableMarkAsDelivered = false, - renderUserProfile = null, - onUserProfileMessage = null, + renderUserProfile, + onUserProfileMessage, breakpoint = false, isUserIdUsedForNickname = true, sdkInitParams, @@ -183,12 +176,7 @@ const SendbirdSDK = ({ isMultipleFilesMessageEnabled = false, eventHandlers, }: SendbirdProviderProps): React.ReactElement => { - const { - logLevel = '', - userMention = {}, - isREMUnitEnabled = false, - pubSub: customPubSub, - } = config; + const { logLevel = '', userMention = {}, isREMUnitEnabled = false, pubSub: customPubSub } = config; const { isMobile } = useMediaQueryContext(); const [logger, setLogger] = useState(LoggerFactory(logLevel as LogLevel)); const [pubSub] = useState(() => customPubSub ?? pubSubFactory()); @@ -199,19 +187,15 @@ const SendbirdSDK = ({ const { configs, configsWithAppAttr, initDashboardConfigs } = useUIKitConfig(); const sdkInitialized = sdkStore.initialized; const sdk = sdkStore?.sdk; - const { - uploadSizeLimit, - multipleFilesMessageFileCountLimit, - } = sdk?.appInfo ?? {}; + const { uploadSizeLimit, multipleFilesMessageFileCountLimit } = sdk?.appInfo ?? {}; useTheme(colorSet); - const { - getCachedTemplate, - updateMessageTemplatesInfo, - initializeMessageTemplatesInfo, - } = useMessageTemplateUtils({ - sdk, logger, appInfoStore, appInfoDispatcher, + const { getCachedTemplate, updateMessageTemplatesInfo, initializeMessageTemplatesInfo } = useMessageTemplateUtils({ + sdk, + logger, + appInfoStore, + appInfoDispatcher, }); const utils: SendbirdProviderUtils = { @@ -225,7 +209,8 @@ const SendbirdSDK = ({ accessToken, isUserIdUsedForNickname, isMobile, - }, { + }, + { logger, nickname, profileUrl, @@ -241,7 +226,8 @@ const SendbirdSDK = ({ initDashboardConfigs, eventHandlers, initializeMessageTemplatesInfo, - }); + }, + ); useUnmount(() => { if (typeof sdk.disconnect === 'function') { @@ -267,9 +253,9 @@ const SendbirdSDK = ({ useEffect(() => { const body = document.querySelector('body'); - body.classList.remove('sendbird-experimental__rem__units'); + body?.classList.remove('sendbird-experimental__rem__units'); if (isREMUnitEnabled) { - body.classList.add('sendbird-experimental__rem__units'); + body?.classList.add('sendbird-experimental__rem__units'); } }, [isREMUnitEnabled]); // add-remove theme from body @@ -277,9 +263,9 @@ const SendbirdSDK = ({ logger.info('Setup theme', `Theme: ${currentTheme}`); try { const body = document.querySelector('body'); - body.classList.remove('sendbird-theme--light'); - body.classList.remove('sendbird-theme--dark'); - body.classList.add(`sendbird-theme--${currentTheme || 'light'}`); + body?.classList.remove('sendbird-theme--light'); + body?.classList.remove('sendbird-theme--dark'); + body?.classList.add(`sendbird-theme--${currentTheme || 'light'}`); logger.info('Finish setup theme'); // eslint-disable-next-line no-empty } catch (e) { @@ -288,10 +274,10 @@ const SendbirdSDK = ({ return () => { try { const body = document.querySelector('body'); - body.classList.remove('sendbird-theme--light'); - body.classList.remove('sendbird-theme--dark'); + body?.classList.remove('sendbird-theme--light'); + body?.classList.remove('sendbird-theme--dark'); // eslint-disable-next-line no-empty - } catch { } + } catch {} }; }, [currentTheme]); @@ -301,13 +287,7 @@ const SendbirdSDK = ({ const markAsDeliveredScheduler = useMarkAsDeliveredScheduler({ isConnected: isOnline }, { logger }); const localeStringSet = React.useMemo(() => { - if (!stringSet) { - return getStringSet('en'); - } - return { - ...getStringSet('en'), - ...stringSet, - }; + return { ...getStringSet('en'), ...stringSet }; }, [stringSet]); /** @@ -317,9 +297,6 @@ const SendbirdSDK = ({ const uikitMultipleFilesMessageLimit = useMemo(() => { return Math.min(DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT, multipleFilesMessageFileCountLimit ?? Number.MAX_SAFE_INTEGER); }, [multipleFilesMessageFileCountLimit]); - const uikitUploadSizeLimit = useMemo(() => { - return uploadSizeLimit; - }, [uploadSizeLimit]); // Emoji Manager const emojiManager = useMemo(() => { @@ -356,7 +333,7 @@ const SendbirdSDK = ({ setCurrentTheme, setCurrenttheme: setCurrentTheme, // deprecated: typo isMultipleFilesMessageEnabled, - uikitUploadSizeLimit, + uikitUploadSizeLimit: uploadSizeLimit ?? DEFAULT_UPLOAD_SIZE_LIMIT, uikitMultipleFilesMessageLimit, userListQuery, logger, @@ -423,9 +400,7 @@ const SendbirdSDK = ({ - - {children} - + {children} diff --git a/src/lib/SendbirdSdkContext.tsx b/src/lib/SendbirdSdkContext.tsx index 5cb499350..bd3b867b7 100644 --- a/src/lib/SendbirdSdkContext.tsx +++ b/src/lib/SendbirdSdkContext.tsx @@ -8,9 +8,11 @@ type ContextAwareComponentType = { export const SendbirdSdkContext = React.createContext(null); -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +/** + * @deprecated This function is deprecated. Use `useSendbirdStateContext` instead. + * */ const withSendbirdContext = (OriginalComponent: any, mapStoreToProps: Record): ContextAwareComponentType => { - const ContextAwareComponent = (props) => ( + const ContextAwareComponent = (props: any) => ( {(context) => { if (mapStoreToProps && typeof mapStoreToProps !== 'function') { @@ -20,7 +22,6 @@ const withSendbirdContext = (OriginalComponent: any, mapStoreToProps: Record; }} diff --git a/src/lib/UserProfileContext.tsx b/src/lib/UserProfileContext.tsx index af4eb3aee..4fa117202 100644 --- a/src/lib/UserProfileContext.tsx +++ b/src/lib/UserProfileContext.tsx @@ -16,8 +16,6 @@ interface UserProfileContextInterface { const UserProfileContext = React.createContext({ disableUserProfile: true, isOpenChannel: false, - renderUserProfile: null, - onUserProfileMessage: null, }); export type UserProfileProviderProps = React.PropsWithChildren<{ @@ -30,8 +28,8 @@ export type UserProfileProviderProps = React.PropsWithChildren<{ const UserProfileProvider = ({ isOpenChannel = false, disableUserProfile = false, - renderUserProfile = null, - onUserProfileMessage = null, + renderUserProfile, + onUserProfileMessage, children, }: UserProfileProviderProps) => { return ( diff --git a/src/lib/emojiManager.tsx b/src/lib/emojiManager.tsx index a974c0a83..cc9a2c664 100644 --- a/src/lib/emojiManager.tsx +++ b/src/lib/emojiManager.tsx @@ -23,7 +23,7 @@ export interface EmojiManagerParams { } export class EmojiManager { - private _emojiContainer: EmojiContainer; + private _emojiContainer!: EmojiContainer; constructor(props: EmojiManagerParams) { const { sdk, logger } = props; @@ -58,7 +58,7 @@ export class EmojiManager { } public getEmojiUrl(reactionKey: Reaction['key']) { - return this.AllEmojisAsArray.find((emoji) => emoji.key === reactionKey).url ?? ''; + return this.AllEmojisAsArray.find((emoji) => emoji.key === reactionKey)?.url ?? ''; } public get emojiContainer() { diff --git a/src/lib/hooks/useOnlineStatus.ts b/src/lib/hooks/useOnlineStatus.ts index 0976e22a0..e0f08bb2d 100644 --- a/src/lib/hooks/useOnlineStatus.ts +++ b/src/lib/hooks/useOnlineStatus.ts @@ -78,14 +78,14 @@ function useOnlineStatus(sdk: SendbirdChat, logger: LoggerInterface) { const body = document.querySelector('body'); if (!isOnline && !sdk?.isCacheEnabled) { try { - body.classList.add('sendbird__offline'); + body?.classList.add('sendbird__offline'); logger.info('Added class sendbird__offline to body'); } catch (e) { // } } else { try { - body.classList.remove('sendbird__offline'); + body?.classList.remove('sendbird__offline'); logger.info('Removed class sendbird__offline from body'); } catch (e) { // diff --git a/src/lib/hooks/useTheme.ts b/src/lib/hooks/useTheme.ts index 8b292758a..bc8fd048f 100644 --- a/src/lib/hooks/useTheme.ts +++ b/src/lib/hooks/useTheme.ts @@ -1,7 +1,7 @@ import { useLayoutEffect } from 'react'; import cssVars from 'css-vars-ponyfill'; -const isEmpty = (obj) => { +const isEmpty = (obj?: null | Record) => { if (obj === null || obj === undefined) { return true; } @@ -14,7 +14,7 @@ const isEmpty = (obj) => { return JSON.stringify(obj) === JSON.stringify({}); }; -const useTheme = (overrides: Record): void => { +const useTheme = (overrides?: Record): void => { useLayoutEffect(() => { if (!isEmpty(overrides)) { cssVars({ diff --git a/src/lib/pubSub/topics.ts b/src/lib/pubSub/topics.ts index 631b9008f..0d9146f53 100644 --- a/src/lib/pubSub/topics.ts +++ b/src/lib/pubSub/topics.ts @@ -86,13 +86,13 @@ export type SBUGlobalPubSubTopicPayloadUnion = | { topic: PUBSUB_TOPICS.LEAVE_CHANNEL; payload: { - channel: GroupChannel | OpenChannel; + channel: GroupChannel; }; } | { topic: PUBSUB_TOPICS.CREATE_CHANNEL; payload: { - channel: GroupChannel | OpenChannel; + channel: GroupChannel; }; } | { diff --git a/src/lib/selectors.ts b/src/lib/selectors.ts index 846f14d92..6f33e11b9 100644 --- a/src/lib/selectors.ts +++ b/src/lib/selectors.ts @@ -421,29 +421,29 @@ export class UikitMessageHandler { this._onPending(message); } - public triggerFailed(error: Error, message: T): void { - this._onFailed(error, message.isResendable ? message : null); + public triggerFailed(error: Error, message: T | null): void { + this._onFailed(error, message?.isResendable ? message : null); } public triggerSucceeded(message: T): void { this._onSucceeded(message); } - public onPending(handler: MessageHandler): UikitMessageHandler { + public onPending(handler: MessageHandler): UikitMessageHandler { if (typeof handler === 'function') { this._onPending = handler; } return this; } - public onFailed(handler: FailedMessageHandler): UikitMessageHandler { + public onFailed(handler: FailedMessageHandler): UikitMessageHandler { if (typeof handler === 'function') { this._onFailed = handler; } return this; } - public onSucceeded(handler: MessageHandler): UikitMessageHandler { + public onSucceeded(handler: MessageHandler): UikitMessageHandler { if (typeof handler === 'function') { this._onSucceeded = handler; } @@ -472,7 +472,7 @@ export const getSendUserMessage = (state: SendBirdState, publishingModules: Publ topics.SEND_MESSAGE_FAILED, { error, message: message as UserMessage, channel, publishingModules }, ); - handler.triggerFailed(error, message); + handler.triggerFailed(error, message as SendableMessage); }) .onPending((message) => { pubSub.publish( diff --git a/src/lib/types.ts b/src/lib/types.ts index 2fdc9bb24..adcb00691 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -68,15 +68,15 @@ export interface SendBirdStateConfig { isOnline: boolean; userId: string; appId: string; - accessToken: string; + accessToken?: string; theme: string; pubSub: SBUGlobalPubSub; logger: Logger; setCurrentTheme: (theme: 'light' | 'dark') => void; - userListQuery?(): UserListQuery; + userListQuery?: () => UserListQuery; uikitUploadSizeLimit: number; uikitMultipleFilesMessageLimit: number; - voiceRecord?: { + voiceRecord: { maxRecordingTime: number; minRecordingTime: number; }; @@ -84,7 +84,7 @@ export interface SendBirdStateConfig { maxMentionCount: number, maxSuggestionCount: number, }; - imageCompression?: ImageCompressionOptions; + imageCompression: ImageCompressionOptions; markAsReadScheduler: MarkAsReadSchedulerType; markAsDeliveredScheduler: MarkAsDeliveredSchedulerType; disableMarkAsDelivered: boolean; @@ -176,7 +176,7 @@ export type SendBirdState = { }, // Customer provided callbacks eventHandlers?: SBUEventHandlers; - emojiManager?: EmojiManager; + emojiManager: EmojiManager; utils: SendbirdProviderUtils; }; diff --git a/src/modules/App/AppLayout.tsx b/src/modules/App/AppLayout.tsx index 62fe9072c..62aa7e252 100644 --- a/src/modules/App/AppLayout.tsx +++ b/src/modules/App/AppLayout.tsx @@ -10,9 +10,7 @@ import useSendbirdStateContext from '../../hooks/useSendbirdStateContext'; import { SendableMessageType } from '../../utils'; import { getCaseResolvedReplyType } from '../../lib/utils/resolvedReplyType'; -export const AppLayout: React.FC = ( - props: AppLayoutProps, -) => { +export const AppLayout = (props: AppLayoutProps) => { const { isMessageGroupingEnabled, allowProfileEdit, diff --git a/src/modules/App/DesktopLayout.tsx b/src/modules/App/DesktopLayout.tsx index a3ded1470..3787cd889 100644 --- a/src/modules/App/DesktopLayout.tsx +++ b/src/modules/App/DesktopLayout.tsx @@ -3,11 +3,11 @@ import { GroupChannel as GroupChannelClass } from '@sendbird/chat/groupChannel'; import type { DesktopLayoutProps } from './types'; -import GroupChannel from '../GroupChannel'; -import GroupChannelList from '../GroupChannelList'; +import GroupChannel, { GroupChannelProps } from '../GroupChannel'; +import GroupChannelList, { GroupChannelListProps } from '../GroupChannelList'; -import Channel from '../Channel'; -import ChannelList from '../ChannelList'; +import Channel, { ChannelProps } from '../Channel'; +import ChannelList, { ChannelListProps } from '../ChannelList'; import ChannelSettings from '../ChannelSettings'; import MessageSearchPannel from '../MessageSearch'; import Thread from '../Thread'; @@ -46,7 +46,7 @@ export const DesktopLayout: React.FC = (props: DesktopLayout if (channel) { setCurrentChannel(channel); } else { - setCurrentChannel(null); + setCurrentChannel(null ?? undefined); } }; @@ -60,7 +60,7 @@ export const DesktopLayout: React.FC = (props: DesktopLayout } }; - const channelListProps = { + const channelListProps: GroupChannelListProps & ChannelListProps = { allowProfileEdit, activeChannelUrl: currentChannel?.url, onProfileEditSuccess: onProfileEditSuccess, @@ -72,7 +72,7 @@ export const DesktopLayout: React.FC = (props: DesktopLayout onUserProfileUpdated: onProfileEditSuccess, }; - const channelProps = { + const channelProps: ChannelProps & GroupChannelProps = { channelUrl: currentChannel?.url || '', onChatHeaderActionClick: () => { setShowSearch(false); @@ -97,7 +97,7 @@ export const DesktopLayout: React.FC = (props: DesktopLayout animatedMessage: highlightedMessage, onMessageAnimated: () => setHighlightedMessage?.(null), showSearchIcon: showSearchIcon, - startingPoint: startingPoint, + startingPoint: startingPoint ?? undefined, isReactionEnabled: isReactionEnabled, replyType: replyType, isMessageGroupingEnabled: isMessageGroupingEnabled, diff --git a/src/modules/App/MobileLayout.tsx b/src/modules/App/MobileLayout.tsx index 18040e09b..53c7cd5a7 100644 --- a/src/modules/App/MobileLayout.tsx +++ b/src/modules/App/MobileLayout.tsx @@ -6,9 +6,9 @@ import { GroupChannelHandler, GroupChannel as GroupChannelClass } from '@sendbir import type { MobileLayoutProps } from './types'; -import GroupChannel from '../GroupChannel'; +import GroupChannel, { GroupChannelProps } from '../GroupChannel'; import GroupChannelList from '../GroupChannelList'; -import Channel from '../Channel'; +import Channel, { ChannelProps } from '../Channel'; import ChannelList from '../ChannelList'; import ChannelSettings from '../ChannelSettings'; import MessageSearch from '../MessageSearch'; @@ -62,7 +62,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro useEffect(() => { if (panel !== PANELS.CHANNEL) { - goToMessage(null, () => setHighlightedMessage(null)); + goToMessage(null, () => setHighlightedMessage?.(null)); } }, [panel]); @@ -106,7 +106,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro onProfileEditSuccess: onProfileEditSuccess, disableAutoSelect: true, onChannelSelect: (channel: GroupChannelClass | null) => { - setCurrentChannel(channel); + setCurrentChannel(channel ?? undefined); if (channel) { setPanel(PANELS.CHANNEL); } else { @@ -121,7 +121,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro onUserProfileUpdated: onProfileEditSuccess, }; - const channelProps = { + const channelProps: ChannelProps & GroupChannelProps = { channelUrl: currentChannel?.url || '', onChatHeaderActionClick: () => { setPanel(PANELS.CHANNEL_SETTINGS); @@ -149,7 +149,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro animatedMessage: highlightedMessage, onMessageAnimated: () => setHighlightedMessage?.(null), showSearchIcon, - startingPoint, + startingPoint: startingPoint ?? undefined, isReactionEnabled, replyType, isMessageGroupingEnabled, @@ -218,7 +218,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro setCurrentChannel(channel); goToMessage(message, (messageId) => { setPanel(PANELS.CHANNEL); - setHighlightedMessage(messageId); + setHighlightedMessage?.(messageId); }); }} /> diff --git a/src/modules/App/index.tsx b/src/modules/App/index.tsx index 4811089c1..ae5852f1f 100644 --- a/src/modules/App/index.tsx +++ b/src/modules/App/index.tsx @@ -12,6 +12,7 @@ import { AppLayout } from './AppLayout'; import './index.scss'; import { AppLayoutProps } from './types'; +import { GroupChannel } from '@sendbird/chat/groupChannel'; export interface AppProps { appId: SendbirdProviderProps['appId']; @@ -75,21 +76,21 @@ export default function App(props: AppProps) { accessToken = '', customApiHost = '', customWebSocketHost = '', - breakpoint = null, + breakpoint, theme = 'light', - userListQuery = null, + userListQuery, nickname = '', profileUrl = '', - dateLocale = null, + dateLocale, config = {}, voiceRecord, isMessageGroupingEnabled = true, - colorSet = null, - stringSet = null, + colorSet, + stringSet, allowProfileEdit = false, disableMarkAsDelivered = false, - renderUserProfile = null, - onProfileEditSuccess = null, + renderUserProfile, + onProfileEditSuccess, imageCompression = {}, disableAutoSelect = false, sdkInitParams, @@ -111,7 +112,7 @@ export default function App(props: AppProps) { isTypingIndicatorEnabledOnChannelList, isMessageReceiptStatusEnabledOnChannelList, } = props; - const [currentChannel, setCurrentChannel] = useState(null); + const [currentChannel, setCurrentChannel] = useState(); return ( ; + setCurrentChannel: React.Dispatch; enableLegacyChannelModules: boolean; } diff --git a/src/modules/Channel/components/ChannelUI/index.tsx b/src/modules/Channel/components/ChannelUI/index.tsx index 1f8a5afa0..e992fd984 100644 --- a/src/modules/Channel/components/ChannelUI/index.tsx +++ b/src/modules/Channel/components/ChannelUI/index.tsx @@ -7,6 +7,8 @@ import ChannelHeader from '../ChannelHeader'; import MessageList from '../MessageList'; import MessageInputWrapper from '../MessageInputWrapper'; import { deleteNullish } from '../../../../utils/utils'; +import { GroupChannelHeaderProps } from '../../../GroupChannel/components/GroupChannelHeader'; +import { GroupChannelMessageListProps } from '../../../GroupChannel/components/MessageList'; export interface ChannelUIProps extends GroupChannelUIBasicProps { isLoading?: boolean; @@ -22,8 +24,8 @@ const ChannelUI = (props: ChannelUIProps) => { // Inject components to presentation layer const { - renderChannelHeader = (p) => , - renderMessageList = (p) => , + renderChannelHeader = (p: GroupChannelHeaderProps) => , + renderMessageList = (p: GroupChannelMessageListProps) => , renderMessageInput = () => , } = deleteNullish(props); diff --git a/src/modules/Channel/components/Message/index.tsx b/src/modules/Channel/components/Message/index.tsx index 028893090..a63a5f2cc 100644 --- a/src/modules/Channel/components/Message/index.tsx +++ b/src/modules/Channel/components/Message/index.tsx @@ -8,7 +8,7 @@ import MessageView, { MessageProps } from '../../../GroupChannel/components/Mess import FileViewer from '../FileViewer'; import RemoveMessageModal from '../RemoveMessageModal'; -const Message = (props: MessageProps): React.ReactElement => { +const Message = (props: MessageProps) => { const { config } = useSendbirdStateContext(); const { initialized, @@ -36,6 +36,8 @@ const Message = (props: MessageProps): React.ReactElement => { const { message } = props; + if (!currentGroupChannel) return null; + return ( { updateUserMessage={(messageId, params) => { updateMessage({ messageId, - message: params.message, + message: params.message ?? '', mentionedUsers: params.mentionedUsers, mentionTemplate: params.mentionedMessageTemplate, }); diff --git a/src/modules/Channel/components/MessageInputWrapper/index.tsx b/src/modules/Channel/components/MessageInputWrapper/index.tsx index cae43fe01..d0983f9d5 100644 --- a/src/modules/Channel/components/MessageInputWrapper/index.tsx +++ b/src/modules/Channel/components/MessageInputWrapper/index.tsx @@ -33,7 +33,7 @@ export const MessageInputWrapper = (props: MessageInputWrapperProps) => { && getSuggestedReplies(lastMessage).length > 0 && localMessages?.length === 0; const disableMessageInput = props.disabled - || isLastMessageSuggestedRepliesEnabled && !!lastMessage.extendedMessagePayload?.['disable_chat_input']; + || isLastMessageSuggestedRepliesEnabled && !!lastMessage?.extendedMessagePayload?.['disable_chat_input']; return ( { message: params.message, mentionTemplate: params.mentionedMessageTemplate, mentionedUsers: params.mentionedUsers, - quoteMessage, + quoteMessage: quoteMessage ?? undefined, }); }} sendFileMessage={(params) => { - return sendFileMessage(params.file as File, quoteMessage); + return sendFileMessage(params.file as File, quoteMessage ?? undefined); }} sendVoiceMessage={({ file }, duration) => { - return sendVoiceMessage(file as File, duration, quoteMessage); + return sendVoiceMessage(file as File, duration, quoteMessage ?? undefined); }} sendMultipleFilesMessage={({ fileInfoList }) => { - return sendMultipleFilesMessage(fileInfoList.map((fileInfo) => fileInfo.file) as File[], quoteMessage); + return sendMultipleFilesMessage(fileInfoList.map((fileInfo) => fileInfo.file) as File[], quoteMessage ?? undefined); }} /> ); diff --git a/src/modules/Channel/components/MessageList/index.tsx b/src/modules/Channel/components/MessageList/index.tsx index 9918ae73c..7b4ce2d78 100644 --- a/src/modules/Channel/components/MessageList/index.tsx +++ b/src/modules/Channel/components/MessageList/index.tsx @@ -72,7 +72,7 @@ export const MessageList = (props: MessageListProps) => { } = useChannelContext(); const store = useSendbirdStateContext(); - const allMessagesFiltered = typeof filterMessageList === 'function' ? allMessages.filter(filterMessageList as (message: EveryMessage) => boolean) : allMessages; + const allMessagesFiltered = typeof filterMessageList === 'function' ? allMessages.filter(filterMessageList) : allMessages; const markAsReadScheduler = store.config.markAsReadScheduler; const [isScrollBottom, setIsScrollBottom] = useState(false); @@ -111,8 +111,8 @@ export const MessageList = (props: MessageListProps) => { setInitialTimeStamp?.(null); setAnimatedMessageId?.(null); setHighLightedMessageId?.(null); - if (scrollRef?.current?.scrollTop > -1) { - scrollRef.current.scrollTop = (scrollRef?.current?.scrollHeight ?? 0) - (scrollRef?.current?.offsetHeight ?? 0); + if (scrollRef.current && scrollRef.current.scrollTop > -1) { + scrollRef.current.scrollTop = (scrollRef.current.scrollHeight ?? 0) - (scrollRef.current.offsetHeight ?? 0); } }; diff --git a/src/modules/Channel/context/ChannelProvider.tsx b/src/modules/Channel/context/ChannelProvider.tsx index 135834be5..61ba44e0d 100644 --- a/src/modules/Channel/context/ChannelProvider.tsx +++ b/src/modules/Channel/context/ChannelProvider.tsx @@ -129,25 +129,25 @@ export type SendMessageType = (params: SendMessageParams) => void; export type UpdateMessageType = (props: UpdateMessageParams, callback?: (err: SendbirdError, message: UserMessage) => void) => void; export interface ChannelProviderInterface extends ChannelContextProps, MessageStoreInterface { - scrollToMessage?(createdAt: number, messageId: number): void; + scrollToMessage(createdAt: number, messageId: number): void; isScrolled?: boolean; setIsScrolled?: React.Dispatch>; messageActionTypes: typeof channelActions; messagesDispatcher: React.Dispatch; quoteMessage: SendableMessageType | null; setQuoteMessage: React.Dispatch>; - initialTimeStamp: number; - setInitialTimeStamp: React.Dispatch>; - animatedMessageId: number; - highLightedMessageId: number; + initialTimeStamp: number | null | undefined; + setInitialTimeStamp: React.Dispatch>; + animatedMessageId: number | null; + highLightedMessageId: number | null | undefined; nicknamesMap: Map; emojiAllMap: any; onScrollCallback: (callback: () => void) => void; onScrollDownCallback: (callback: (param: [BaseMessage[], null] | [null, unknown]) => void) => void; - scrollRef: React.MutableRefObject; - setAnimatedMessageId: React.Dispatch>; - setHighLightedMessageId: React.Dispatch>; - messageInputRef: React.MutableRefObject, + scrollRef: React.RefObject; + setAnimatedMessageId: React.Dispatch>; + setHighLightedMessageId: React.Dispatch>; + messageInputRef: React.RefObject, deleteMessage(message: CoreMessageType): Promise, updateMessage: UpdateMessageType, resendMessage(failedMessage: SendableMessageType): void, @@ -160,9 +160,9 @@ export interface ChannelProviderInterface extends ChannelContextProps, MessageSt renderUserMentionItem?: (props: { user: User }) => JSX.Element; } -const ChannelContext = React.createContext(undefined); +const ChannelContext = React.createContext(null); -const ChannelProvider: React.FC = (props: ChannelContextProps) => { +const ChannelProvider = (props: ChannelContextProps) => { const { channelUrl, children, @@ -220,11 +220,11 @@ const ChannelProvider: React.FC = (props: ChannelContextPro setHighLightedMessageId(highlightedMessage); }, [highlightedMessage]); const userFilledMessageListQuery = queries?.messageListParams; - const [quoteMessage, setQuoteMessage] = useState(null); + const [quoteMessage, setQuoteMessage] = useState(null); const [isScrolled, setIsScrolled] = useState(false); const [messagesStore, messagesDispatcher] = useReducer(messagesReducer, messagesInitialState); - const scrollRef = useRef(null); + const scrollRef = useRef(null); const isMentionEnabled = groupChannel.enableMention; @@ -519,8 +519,11 @@ const ChannelProvider: React.FC = (props: ChannelContextPro ); }; -export type UseChannelType = () => ChannelProviderInterface; -const useChannelContext: UseChannelType = () => React.useContext(ChannelContext); +const useChannelContext = () => { + const context = React.useContext(ChannelContext); + if (!context) throw new Error('ChannelContext not found. Use within the Channel module.'); + return context; +}; export { ChannelProvider, diff --git a/src/modules/Channel/context/hooks/useDeleteMessageCallback.ts b/src/modules/Channel/context/hooks/useDeleteMessageCallback.ts index 0d6b0f761..c87d546a1 100644 --- a/src/modules/Channel/context/hooks/useDeleteMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useDeleteMessageCallback.ts @@ -36,7 +36,7 @@ function useDeleteMessageCallback( } else { logger.info('Channel | useDeleteMessageCallback: Deleting message from remote:', sendingStatus); currentGroupChannel - .deleteMessage(message) + ?.deleteMessage(message) .then(() => { logger.info('Channel | useDeleteMessageCallback: Deleting message success!', message); messagesDispatcher({ diff --git a/src/modules/Channel/context/hooks/useFileInfoListWithUploaded.ts b/src/modules/Channel/context/hooks/useFileInfoListWithUploaded.ts index 9d67419b7..dc3b29de1 100644 --- a/src/modules/Channel/context/hooks/useFileInfoListWithUploaded.ts +++ b/src/modules/Channel/context/hooks/useFileInfoListWithUploaded.ts @@ -33,12 +33,14 @@ export const useFileInfoListWithUploaded = (message: CoreMessageType): UploadedF ...it, url: it.url, isUploaded: true, + mimeType: it.mimeType ?? undefined, + fileName: it.fileName ?? undefined, })); } else { - return message.messageParams.fileInfoList.map((it, index) => ({ + return message?.messageParams?.fileInfoList.map((it, index) => ({ ...it, url: getObjectURL(index) ?? it.fileUrl ?? (it.file instanceof Blob ? getObjectURL(index, it.file) : undefined), isUploaded: !it.file && typeof it.fileUrl === 'string' && it.fileUrl.length > 0, - })); + })) ?? []; } }; diff --git a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts index a89eeb35b..ba1e7f34e 100644 --- a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts +++ b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts @@ -23,14 +23,14 @@ import { SdkStore } from '../../../../lib/types'; interface DynamicParams { sdkInit: boolean; currentUserId: string; - currentGroupChannel: GroupChannel; + currentGroupChannel: GroupChannel | null; disableMarkAsRead: boolean; } interface StaticParams { sdk: SdkStore['sdk']; logger: LoggerInterface; scrollRef: React.RefObject; - setQuoteMessage: React.Dispatch>; + setQuoteMessage: React.Dispatch>; messagesDispatcher: React.Dispatch; } @@ -66,7 +66,9 @@ function useHandleChannelEvents({ let scrollToEnd = false; try { const { current } = scrollRef; - scrollToEnd = current.offsetHeight + current.scrollTop >= current.scrollHeight - 10; + if (current) { + scrollToEnd = current.offsetHeight + current.scrollTop >= current.scrollHeight - 10; + } // 10 is a buffer } catch (error) { // @@ -193,8 +195,8 @@ function useHandleChannelEvents({ }); } }, - onUserBanned: (channel: GroupChannel, user) => { - if (compareIds(channel?.url, channelUrl)) { + onUserBanned: (channel, user) => { + if (compareIds(channel?.url, channelUrl) && channel.isGroupChannel()) { logger.info('Channel | useHandleChannelEvents: onUserBanned', { channel, user }); const isByMe = user?.userId === sdk?.currentUser?.userId; messagesDispatcher({ diff --git a/src/modules/Channel/context/hooks/useHandleReconnect.ts b/src/modules/Channel/context/hooks/useHandleReconnect.ts index ab83f4be1..6672afd07 100644 --- a/src/modules/Channel/context/hooks/useHandleReconnect.ts +++ b/src/modules/Channel/context/hooks/useHandleReconnect.ts @@ -23,7 +23,7 @@ interface DynamicParams { interface StaticParams { logger: Logger; sdk: SdkStore['sdk']; - currentGroupChannel: GroupChannel; + currentGroupChannel: GroupChannel | null; scrollRef: React.RefObject; markAsReadScheduler: MarkAsReadSchedulerType; messagesDispatcher: React.Dispatch; @@ -65,6 +65,7 @@ function useHandleReconnect( } if (userFilledMessageListQuery) { Object.keys(userFilledMessageListQuery).forEach((key) => { + // @ts-ignore messageListParams[key] = userFilledMessageListQuery[key]; }); } @@ -74,7 +75,7 @@ function useHandleReconnect( payload: null, }); - sdk?.groupChannel?.getChannel(currentGroupChannel?.url) + sdk?.groupChannel?.getChannel(currentGroupChannel?.url ?? '') .then((groupChannel) => { const lastMessageTime = new Date().getTime(); @@ -86,7 +87,7 @@ function useHandleReconnect( messagesDispatcher({ type: messageActionTypes.FETCH_INITIAL_MESSAGES_SUCCESS, payload: { - currentGroupChannel, + currentGroupChannel: groupChannel, messages: messages as CoreMessageType[], }, }); @@ -96,11 +97,11 @@ function useHandleReconnect( logger.error('Channel: Fetching messages failed', error); messagesDispatcher({ type: messageActionTypes.FETCH_INITIAL_MESSAGES_FAILURE, - payload: { currentGroupChannel }, + payload: { currentGroupChannel: groupChannel }, }); }); if (!disableMarkAsRead) { - markAsReadScheduler.push(currentGroupChannel); + markAsReadScheduler.push(groupChannel); } }); } diff --git a/src/modules/Channel/context/hooks/useHandleReconnectForChannelList.ts b/src/modules/Channel/context/hooks/useHandleReconnectForChannelList.ts index 1ebb21318..5bc91eab3 100644 --- a/src/modules/Channel/context/hooks/useHandleReconnectForChannelList.ts +++ b/src/modules/Channel/context/hooks/useHandleReconnectForChannelList.ts @@ -17,16 +17,16 @@ import { DELIVERY_RECEIPT } from '../../../../utils/consts'; interface UseHandleReconnectForChannelListProps { // Dynamic props isOnline: boolean; - reconnectOnIdle: boolean; + reconnectOnIdle?: boolean; // Static props logger: Logger; sdk: SdkStore['sdk']; - currentGroupChannel: GroupChannel; + currentGroupChannel: GroupChannel | null; channelListDispatcher: React.Dispatch setChannelSource: (query: GroupChannelListQuery) => void; - userFilledChannelListQuery: GroupChannelListQueryParamsInternal; - sortChannelList: (channels: GroupChannel[]) => GroupChannel[]; + userFilledChannelListQuery?: GroupChannelListQueryParamsInternal; + sortChannelList?: (channels: GroupChannel[]) => GroupChannel[]; disableAutoSelect: boolean; markAsDeliveredScheduler: MarkAsDeliveredSchedulerType; disableMarkAsDelivered: boolean; @@ -87,7 +87,7 @@ function useHandleReconnectForChannelList({ logger.info('ChannelList refresh - channel list sorted', sortedChannelList); } // select first channel - let newCurrentChannel: GroupChannel = !disableAutoSelect ? sortedChannelList[0] : null; + let newCurrentChannel: GroupChannel | null = !disableAutoSelect ? sortedChannelList[0] : null; if (currentGroupChannel?.url) { const foundChannel = sortedChannelList.find((channel) => ( channel.url === currentGroupChannel.url diff --git a/src/modules/Channel/context/hooks/useInitialMessagesFetch.ts b/src/modules/Channel/context/hooks/useInitialMessagesFetch.ts index 87465e0b3..abf26a3e8 100644 --- a/src/modules/Channel/context/hooks/useInitialMessagesFetch.ts +++ b/src/modules/Channel/context/hooks/useInitialMessagesFetch.ts @@ -13,9 +13,9 @@ import { ChannelActionTypes } from '../dux/actionTypes'; import { SCROLL_BOTTOM_DELAY_FOR_FETCH } from '../../../../utils/consts'; type UseInitialMessagesFetchOptions = { - currentGroupChannel: GroupChannel; - initialTimeStamp: number; - userFilledMessageListQuery: MessageListParamsInternal; + currentGroupChannel: GroupChannel | null; + initialTimeStamp: number | null | undefined; + userFilledMessageListQuery?: MessageListParamsInternal; replyType: ReplyTypeInternal; setIsScrolled: (val: boolean) => void; }; @@ -70,6 +70,7 @@ function useInitialMessagesFetch( } if (userFilledMessageListQuery) { Object.keys(userFilledMessageListQuery).forEach((key) => { + // @ts-ignore messageListParams[key] = userFilledMessageListQuery[key]; }); } diff --git a/src/modules/Channel/context/hooks/useReconnectOnIdle.ts b/src/modules/Channel/context/hooks/useReconnectOnIdle.ts index 12aed033e..b03a3aef7 100644 --- a/src/modules/Channel/context/hooks/useReconnectOnIdle.ts +++ b/src/modules/Channel/context/hooks/useReconnectOnIdle.ts @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; import type { GroupChannel } from '@sendbird/chat/groupChannel'; -function useReconnectOnIdle(isOnline: boolean, currentGroupChannel: GroupChannel, reconnectOnIdle = true) - :{ shouldReconnect: boolean } -{ +function useReconnectOnIdle( + isOnline: boolean, + currentGroupChannel: GroupChannel | null, + reconnectOnIdle = true, +): { shouldReconnect: boolean } { const [isTabHidden, setIsTabHidden] = useState(false); const wasOffline = !isOnline; @@ -20,7 +22,7 @@ function useReconnectOnIdle(isOnline: boolean, currentGroupChannel: GroupChannel }; }, [reconnectOnIdle, document.hidden]); - const shouldReconnect = wasOffline && currentGroupChannel?.url != null && !isTabHidden; + const shouldReconnect = wasOffline && !!currentGroupChannel && !isTabHidden; return { shouldReconnect }; } diff --git a/src/modules/Channel/context/hooks/useResendMessageCallback.ts b/src/modules/Channel/context/hooks/useResendMessageCallback.ts index c3f496f85..f99f223d6 100644 --- a/src/modules/Channel/context/hooks/useResendMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useResendMessageCallback.ts @@ -28,7 +28,7 @@ function useResendMessageCallback( // userMessage if (failedMessage.isUserMessage()) { currentGroupChannel - .resendMessage(failedMessage) + ?.resendMessage(failedMessage) .onPending((message) => { logger.info('Channel: Resending message start!', message); messagesDispatcher({ @@ -52,7 +52,7 @@ function useResendMessageCallback( }); } else if (failedMessage.isFileMessage()) { currentGroupChannel - .resendMessage(failedMessage) + ?.resendMessage(failedMessage) .onPending((message) => { logger.info('Channel: Resending file message start!', message); messagesDispatcher({ @@ -76,7 +76,7 @@ function useResendMessageCallback( }); } else if (failedMessage.isMultipleFilesMessage()) { currentGroupChannel - .resendMessage(failedMessage) + ?.resendMessage(failedMessage) .onPending((message) => { logger.info('Channel: Resending multiple files message start!', message); messagesDispatcher({ @@ -93,7 +93,7 @@ function useResendMessageCallback( }); pubSub.publish(topics.ON_FILE_INFO_UPLOADED, { response: { - channelUrl: currentGroupChannel.url, + channelUrl: currentGroupChannel?.url ?? '', requestId, index, uploadableFileInfo, diff --git a/src/modules/Channel/context/hooks/useScrollCallback.ts b/src/modules/Channel/context/hooks/useScrollCallback.ts index 513936284..ab676d7c2 100644 --- a/src/modules/Channel/context/hooks/useScrollCallback.ts +++ b/src/modules/Channel/context/hooks/useScrollCallback.ts @@ -12,9 +12,9 @@ import { CoreMessageType } from '../../../../utils'; import { SdkStore } from '../../../../lib/types'; type UseScrollCallbackOptions = { - currentGroupChannel: GroupChannel; + currentGroupChannel: GroupChannel | null; oldestMessageTimeStamp: number; - userFilledMessageListQuery: MessageListParamsInternal; + userFilledMessageListQuery?: MessageListParamsInternal; replyType: ReplyTypeInternal; }; @@ -49,6 +49,7 @@ function useScrollCallback( if (userFilledMessageListQuery) { Object.keys(userFilledMessageListQuery).forEach((key) => { + // @ts-ignore messageListParams[key] = userFilledMessageListQuery[key]; }); } @@ -58,8 +59,7 @@ function useScrollCallback( userFilledMessageListQuery, }); - currentGroupChannel - .getMessagesByTimestamp(oldestMessageTimeStamp || new Date().getTime(), messageListParams as MessageListParams) + currentGroupChannel?.getMessagesByTimestamp(oldestMessageTimeStamp || new Date().getTime(), messageListParams as MessageListParams) .then((messages) => { messagesDispatcher({ type: messageActionTypes.FETCH_PREV_MESSAGES_SUCCESS, diff --git a/src/modules/Channel/context/hooks/useScrollDownCallback.ts b/src/modules/Channel/context/hooks/useScrollDownCallback.ts index 60ba67c97..c61d459ef 100644 --- a/src/modules/Channel/context/hooks/useScrollDownCallback.ts +++ b/src/modules/Channel/context/hooks/useScrollDownCallback.ts @@ -14,7 +14,7 @@ import { CoreMessageType } from '../../../../utils'; type UseScrollDownCallbackOptions = { currentGroupChannel: null | GroupChannel; latestMessageTimeStamp: number; - userFilledMessageListQuery: MessageListParamsInternal; + userFilledMessageListQuery?: MessageListParamsInternal; hasMoreNext: boolean; replyType: ReplyTypeInternal; }; @@ -53,13 +53,13 @@ function useScrollDownCallback( } if (userFilledMessageListQuery) { Object.keys(userFilledMessageListQuery).forEach((key) => { + // @ts-ignore messageListParams[key] = userFilledMessageListQuery[key]; }); } logger.info('Channel: Fetching later messages', { currentGroupChannel, userFilledMessageListQuery }); - currentGroupChannel - .getMessagesByTimestamp(latestMessageTimeStamp || new Date().getTime(), messageListParams as MessageListParams) + currentGroupChannel?.getMessagesByTimestamp(latestMessageTimeStamp || new Date().getTime(), messageListParams as MessageListParams) .then((messages) => { messagesDispatcher({ type: messageActionTypes.FETCH_NEXT_MESSAGES_SUCCESS, diff --git a/src/modules/Channel/context/hooks/useScrollToMessage.ts b/src/modules/Channel/context/hooks/useScrollToMessage.ts index d55747866..94b742bb2 100644 --- a/src/modules/Channel/context/hooks/useScrollToMessage.ts +++ b/src/modules/Channel/context/hooks/useScrollToMessage.ts @@ -1,12 +1,12 @@ import type { BaseMessage } from '@sendbird/chat/message'; -import { useCallback } from 'react'; +import React, { useCallback } from 'react'; import { scrollToRenderedMessage } from '../utils'; import { LoggerInterface } from '../../../../lib/Logger'; interface DynamicParams { - setInitialTimeStamp: React.Dispatch>; - setAnimatedMessageId: React.Dispatch>; + setInitialTimeStamp: React.Dispatch>; + setAnimatedMessageId: React.Dispatch>; allMessages: BaseMessage[]; scrollRef: React.RefObject; } diff --git a/src/modules/Channel/context/hooks/useSendFileMessageCallback.ts b/src/modules/Channel/context/hooks/useSendFileMessageCallback.ts index 8fe8368d6..37cabca5f 100644 --- a/src/modules/Channel/context/hooks/useSendFileMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useSendFileMessageCallback.ts @@ -14,7 +14,7 @@ import { SCROLL_BOTTOM_DELAY_FOR_SEND } from '../../../../utils/consts'; type UseSendFileMessageCallbackOptions = { currentGroupChannel: null | GroupChannel; - onBeforeSendFileMessage?: (file: File, quoteMessage: SendableMessageType | null) => FileMessageCreateParams; + onBeforeSendFileMessage?: (file: File, quoteMessage?: SendableMessageType) => FileMessageCreateParams; imageCompression?: SendBirdState['config']['imageCompression']; }; type UseSendFileMessageCallbackParams = { @@ -28,9 +28,9 @@ export default function useSendFileMessageCallback( { logger, pubSub, scrollRef, messagesDispatcher }: UseSendFileMessageCallbackParams, ) { const sendMessage = useCallback( - (compressedFile: File, quoteMessage = null) => new Promise((resolve, reject) => { + (compressedFile: File, quoteMessage?: SendableMessageType) => new Promise((resolve, reject) => { // Create FileMessageParams - let params: FileMessageCreateParams = onBeforeSendFileMessage?.(compressedFile, quoteMessage); + let params = onBeforeSendFileMessage?.(compressedFile, quoteMessage); if (!params) { params = { file: compressedFile }; if (quoteMessage) { @@ -41,8 +41,7 @@ export default function useSendFileMessageCallback( // Send FileMessage logger.info('Channel: Uploading file message start!', params); - currentGroupChannel - .sendFileMessage(params) + currentGroupChannel?.sendFileMessage(params) .onPending((pendingMessage) => { pubSub.publish(topics.SEND_MESSAGE_START, { /* pubSub is used instead of messagesDispatcher diff --git a/src/modules/Channel/context/hooks/useSendMessageCallback.ts b/src/modules/Channel/context/hooks/useSendMessageCallback.ts index 7bc6983cb..c42f8b568 100644 --- a/src/modules/Channel/context/hooks/useSendMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useSendMessageCallback.ts @@ -49,12 +49,12 @@ export default function useSendMessageCallback( message, }; // if (isMentionEnabled && mentionedUserIds?.length > 0) { - if (isMentionEnabled && mentionedUsers?.length > 0) { + if (isMentionEnabled && mentionedUsers && mentionedUsers.length > 0) { // params.mentionedUserIds = mentionedUserIds; params.mentionedUsers = mentionedUsers; } // if (isMentionEnabled && mentionTemplate && mentionedUserIds?.length > 0) { - if (isMentionEnabled && mentionTemplate && mentionedUsers?.length > 0) { + if (isMentionEnabled && mentionTemplate && mentionedUsers && mentionedUsers.length > 0) { params.mentionedMessageTemplate = mentionTemplate; } if (quoteMessage) { @@ -74,7 +74,7 @@ export default function useSendMessageCallback( logger.info('Channel: Sending message has started', params); currentGroupChannel - .sendUserMessage(params) + ?.sendUserMessage(params) .onPending((pendingMsg) => { pubSub.publish(topics.SEND_MESSAGE_START, { /* pubSub is used instead of messagesDispatcher diff --git a/src/modules/Channel/context/hooks/useSendMultipleFilesMessage.ts b/src/modules/Channel/context/hooks/useSendMultipleFilesMessage.ts index 7dd1bf916..f196b6fdd 100644 --- a/src/modules/Channel/context/hooks/useSendMultipleFilesMessage.ts +++ b/src/modules/Channel/context/hooks/useSendMultipleFilesMessage.ts @@ -83,7 +83,7 @@ export const useSendMultipleFilesMessage = ({ } logger.info('Channel: Start sending MFM', { messageParams }); try { - currentChannel.sendMultipleFilesMessage(messageParams) + currentChannel?.sendMultipleFilesMessage(messageParams) .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { logger.info('Channel: onFileUploaded during sending MFM', { requestId, diff --git a/src/modules/Channel/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Channel/context/hooks/useSendVoiceMessageCallback.ts index 2eef76fe2..1855f5c2b 100644 --- a/src/modules/Channel/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useSendVoiceMessageCallback.ts @@ -30,7 +30,7 @@ interface StaticParams { scrollRef: React.RefObject; messagesDispatcher: React.Dispatch; } -type FuncType = (file: File, duration: number, quoteMessage: SendableMessageType) => Promise; +type FuncType = (file: File, duration: number, quoteMessage?: SendableMessageType) => Promise; export const useSendVoiceMessageCallback = ({ currentGroupChannel, @@ -42,14 +42,11 @@ export const useSendVoiceMessageCallback = ({ scrollRef, messagesDispatcher, }: StaticParams): Array => { - const sendMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType): Promise => new Promise((resolve, reject) => { + const sendMessage = useCallback((file: File, duration: number, quoteMessage?: SendableMessageType): Promise => new Promise((resolve, reject) => { if (!currentGroupChannel) { return; } - const messageParams: FileMessageCreateParams = ( - onBeforeSendVoiceMessage - && typeof onBeforeSendVoiceMessage === 'function' - ) + const messageParams: FileMessageCreateParams = (onBeforeSendVoiceMessage && typeof onBeforeSendVoiceMessage === 'function') ? onBeforeSendVoiceMessage(file, quoteMessage) : { file, @@ -90,13 +87,13 @@ export const useSendVoiceMessageCallback = ({ }); reject(err); }) - .onSucceeded((succeededMessage: FileMessage) => { + .onSucceeded((succeededMessage) => { logger.info('Channel: Sending voice message success!', succeededMessage); messagesDispatcher({ type: messageActionTypes.SEND_MESSAGE_SUCCESS, payload: succeededMessage as SendableMessageType, }); - resolve(succeededMessage); + resolve(succeededMessage as FileMessage); }); }), [ currentGroupChannel, diff --git a/src/modules/Channel/context/hooks/useToggleReactionCallback.ts b/src/modules/Channel/context/hooks/useToggleReactionCallback.ts index f722a4170..ffa984357 100644 --- a/src/modules/Channel/context/hooks/useToggleReactionCallback.ts +++ b/src/modules/Channel/context/hooks/useToggleReactionCallback.ts @@ -17,7 +17,7 @@ export default function useToggleReactionCallback( (message: BaseMessage, key: string, isReacted: boolean) => { if (isReacted) { currentGroupChannel - .deleteReaction(message, key) + ?.deleteReaction(message, key) .then((res) => { logger.info('Delete reaction success', res); }) @@ -26,7 +26,7 @@ export default function useToggleReactionCallback( }); } else { currentGroupChannel - .addReaction(message, key) + ?.addReaction(message, key) .then((res) => { logger.info('Add reaction success', res); }) diff --git a/src/modules/Channel/context/hooks/useUpdateMessageCallback.ts b/src/modules/Channel/context/hooks/useUpdateMessageCallback.ts index b78871363..e4619deff 100644 --- a/src/modules/Channel/context/hooks/useUpdateMessageCallback.ts +++ b/src/modules/Channel/context/hooks/useUpdateMessageCallback.ts @@ -42,7 +42,7 @@ function useUpdateMessageCallback( const params: UserMessageUpdateParams = { message, }; - if (isMentionEnabled && mentionedUsers?.length > 0) { + if (isMentionEnabled && mentionedUsers && mentionedUsers.length > 0) { params.mentionedUsers = mentionedUsers; } if (isMentionEnabled && mentionTemplate) { @@ -63,7 +63,7 @@ function useUpdateMessageCallback( logger.info('Channel: Updating message!', params); currentGroupChannel - .updateUserMessage(messageId, params) + ?.updateUserMessage(messageId, params) .then((msg) => { if (callback) { callback(null, msg); diff --git a/src/modules/Channel/context/utils.ts b/src/modules/Channel/context/utils.ts index cf97a11b0..fa368ddf5 100644 --- a/src/modules/Channel/context/utils.ts +++ b/src/modules/Channel/context/utils.ts @@ -7,15 +7,15 @@ import { CoreMessageType, SendableMessageType } from '../../../utils'; import { BaseMessage, SendingStatus } from '@sendbird/chat/message'; export const scrollToRenderedMessage = ( - scrollRef: React.MutableRefObject, + scrollRef: React.RefObject, initialTimeStamp: number, setIsScrolled?: (val: boolean) => void, ) => { try { const container = scrollRef.current; // scroll into the message with initialTimeStamp - const element = container.querySelectorAll(`[data-sb-created-at="${initialTimeStamp}"]`)?.[0]; - if (element instanceof HTMLElement) { + const element = container?.querySelectorAll(`[data-sb-created-at="${initialTimeStamp}"]`)?.[0]; + if (container && element instanceof HTMLElement) { // Set the scroll position of the container to bring the element to the top container.scrollTop = element.offsetTop; } @@ -29,7 +29,7 @@ export const scrollToRenderedMessage = ( /* eslint-disable default-param-last */ export const scrollIntoLast = ( initialTry = 0, - scrollRef: React.MutableRefObject, + scrollRef: React.RefObject, setIsScrolled?: (val: boolean) => void, ) => { const MAX_TRIES = 10; @@ -40,7 +40,7 @@ export const scrollIntoLast = ( } try { const scrollDOM = scrollRef?.current || document.querySelector('.sendbird-conversation__messages-padding'); - scrollDOM.scrollTop = scrollDOM.scrollHeight; + if (scrollDOM) scrollDOM.scrollTop = scrollDOM.scrollHeight; setIsScrolled?.(true); } catch (error) { setTimeout(() => { @@ -54,12 +54,12 @@ export const isOperator = (groupChannel?: GroupChannel) => { return myRole === 'operator'; }; -export const isDisabledBecauseFrozen = (groupChannel?: GroupChannel) => { +export const isDisabledBecauseFrozen = (groupChannel?: GroupChannel | null) => { const isFrozen = groupChannel?.isFrozen; return isFrozen && !isOperator(groupChannel); }; -export const isDisabledBecauseMuted = (groupChannel?: GroupChannel) => { +export const isDisabledBecauseMuted = (groupChannel?: GroupChannel | null) => { const myMutedState = groupChannel?.myMutedState; return myMutedState === 'muted'; }; diff --git a/src/modules/ChannelList/components/AddChannel/index.tsx b/src/modules/ChannelList/components/AddChannel/index.tsx index d32f56b0a..70d4ab9c9 100644 --- a/src/modules/ChannelList/components/AddChannel/index.tsx +++ b/src/modules/ChannelList/components/AddChannel/index.tsx @@ -12,7 +12,7 @@ export const AddChannel = () => { onChangeCreateChannelVisible={setShowModal} onCreateChannelClick={overrideInviteUser} onBeforeCreateChannel={onBeforeCreateChannel} - onChannelCreated={onChannelSelect} + onChannelCreated={it => onChannelSelect?.(it)} /> ); }; diff --git a/src/modules/ChannelList/components/ChannelListUI/index.tsx b/src/modules/ChannelList/components/ChannelListUI/index.tsx index 3ea7ed7af..10a1a6b57 100644 --- a/src/modules/ChannelList/components/ChannelListUI/index.tsx +++ b/src/modules/ChannelList/components/ChannelListUI/index.tsx @@ -9,9 +9,10 @@ import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { GroupChannelListUIView } from '../../../GroupChannelList/components/GroupChannelListUI/GroupChannelListUIView'; import AddChannel from '../AddChannel'; import { GroupChannelListItemBasicProps } from '../../../GroupChannelList/components/GroupChannelListItem/GroupChannelListItemView'; +import { noop } from '../../../../utils/utils'; interface ChannelPreviewProps extends Omit { - onLeaveChannel(channel?: GroupChannel, onLeaveChannelCb?: (channel: GroupChannel, error?: null) => void): Promise; + onLeaveChannel(channel?: GroupChannel, onLeaveChannelCb?: (channel: GroupChannel, error?: unknown) => void): Promise; } export interface ChannelListUIProps { @@ -61,7 +62,7 @@ const ChannelListUI: React.FC = (props: ChannelListUIProps) payload: channel, }); }, - async onLeaveChannel(channel?: GroupChannel, cb?: (channel: GroupChannel, error?: null) => void) { + async onLeaveChannel(channel?: GroupChannel, cb?: (channel: GroupChannel, error?: unknown) => void) { logger.info('ChannelList: Leaving channel', channel); if (channel) { try { @@ -100,9 +101,9 @@ const ChannelListUI: React.FC = (props: ChannelListUIProps) renderPlaceHolderError={renderPlaceHolderError} renderPlaceHolderLoading={renderPlaceHolderLoading} renderPlaceHolderEmptyList={renderPlaceHolderEmptyList} - onChangeTheme={onThemeChange} + onChangeTheme={onThemeChange ?? noop} allowProfileEdit={allowProfileEdit} - onUserProfileUpdated={onProfileEditSuccess} + onUserProfileUpdated={onProfileEditSuccess ?? noop} channels={allChannels} onLoadMore={fetchChannelList} initialized={initialized} diff --git a/src/modules/ChannelList/context/ChannelListProvider.tsx b/src/modules/ChannelList/context/ChannelListProvider.tsx index a5b908c34..b54a7c60a 100644 --- a/src/modules/ChannelList/context/ChannelListProvider.tsx +++ b/src/modules/ChannelList/context/ChannelListProvider.tsx @@ -49,14 +49,14 @@ export interface GroupChannelListQueryParamsInternal { userIdsExactFilter?: Array; userIdsIncludeFilter?: Array; userIdsIncludeFilterQueryType?: QueryType; - nicknameContainsFilter?: string; + nicknameContainsFilter?: string | null; channelNameContainsFilter?: string; - customTypesFilter?: Array; - customTypeStartsWithFilter?: string; - channelUrlsFilter?: Array; + customTypesFilter?: Array | null; + customTypeStartsWithFilter?: string | null; + channelUrlsFilter?: Array | null; superChannelFilter?: SuperChannelFilter; publicChannelFilter?: PublicChannelFilter; - metadataOrderKeyFilter?: string; + metadataOrderKeyFilter?: string | null; memberStateFilter?: MyMemberStateFilter; hiddenChannelFilter?: HiddenChannelFilter; unreadChannelFilter?: UnreadChannelFilter; @@ -100,35 +100,15 @@ export interface ChannelListProviderInterface extends ChannelListProviderProps { initialized: boolean; loading: boolean; allChannels: GroupChannel[]; - currentChannel: GroupChannel; - channelListQuery: GroupChannelListQueryParamsInternal; + currentChannel: GroupChannel | null; + channelListQuery: GroupChannelListQueryParamsInternal | null; currentUserId: string; channelListDispatcher: React.Dispatch; channelSource: GroupChannelListQuerySb | null; fetchChannelList: () => void; } -const ChannelListContext = React.createContext({ - disableUserProfile: true, - allowProfileEdit: true, - onBeforeCreateChannel: null, - onThemeChange: null, - onProfileEditSuccess: null, - onChannelSelect: null, - queries: {}, - className: null, - initialized: false, - loading: false, - allChannels: [], - currentChannel: null, - channelListQuery: {}, - currentUserId: null, - channelListDispatcher: null, - channelSource: null, - typingChannels: [], - fetchChannelList: noop, - reconnectOnIdle: true, -}); +const ChannelListContext = React.createContext(null); const ChannelListProvider: React.FC = (props: ChannelListProviderProps) => { // destruct props @@ -169,7 +149,7 @@ const ChannelListProvider: React.FC = (props: ChannelL // enable if it is true at least once(both are false by default) const userDefinedDisableUserProfile = disableUserProfile ?? !config.common.enableUsingDefaultUserProfile; const userDefinedRenderProfile = config?.renderUserProfile; - const enableEditProfile = allowProfileEdit || config?.allowProfileEdit; + const enableEditProfile = allowProfileEdit || config.allowProfileEdit; const userFilledChannelListQuery = queries?.channelListQuery; const userFilledApplicationUserListQuery = queries?.applicationUserListQuery; @@ -199,7 +179,7 @@ const ChannelListProvider: React.FC = (props: ChannelL channelListDispatcher, setChannelSource, onChannelSelect, - userFilledChannelListQuery, + userFilledChannelListQuery: { ...userFilledChannelListQuery }, logger, sortChannelList, disableAutoSelect, @@ -409,8 +389,9 @@ const ChannelListProvider: React.FC = (props: ChannelL ); }; -function useChannelListContext(): ChannelListProviderInterface { - const context: ChannelListProviderInterface = useContext(ChannelListContext); +function useChannelListContext() { + const context = useContext(ChannelListContext); + if (!context) throw new Error('ChannelListContext not found. Use within the ChannelList module.'); return context; } diff --git a/src/modules/ChannelList/index.tsx b/src/modules/ChannelList/index.tsx index bc699843e..d49d34861 100644 --- a/src/modules/ChannelList/index.tsx +++ b/src/modules/ChannelList/index.tsx @@ -6,7 +6,7 @@ import { import ChannelListUI, { ChannelListUIProps } from './components/ChannelListUI'; -interface ChannelListProps extends ChannelListProviderProps, ChannelListUIProps {} +export interface ChannelListProps extends ChannelListProviderProps, ChannelListUIProps {} const ChannelList: React.FC = (props: ChannelListProps) => { return ( diff --git a/src/modules/ChannelList/utils.ts b/src/modules/ChannelList/utils.ts index 16439b856..3a2d54245 100644 --- a/src/modules/ChannelList/utils.ts +++ b/src/modules/ChannelList/utils.ts @@ -83,7 +83,7 @@ const createEventHandler = ({ sdk, sdkChannelHandlerId, channelListDispatcher, l } }, onMessageUpdated: (channel, message) => { - if (channel.isGroupChannel() && channel.lastMessage.isEqual(message)) { + if (channel.isGroupChannel() && channel.lastMessage?.isEqual(message)) { logger.info('ChannelList: onMessageUpdated', channel); channelListDispatcher({ type: channelActions.ON_LAST_MESSAGE_UPDATED, @@ -124,7 +124,7 @@ const createEventHandler = ({ sdk, sdkChannelHandlerId, channelListDispatcher, l type CreateChannelListQueryParams = { sdk: SdkStore['sdk']; - userFilledChannelListQuery: GroupChannelListQueryParamsInternal; + userFilledChannelListQuery?: GroupChannelListQueryParamsInternal; }; export const createChannelListQuery = ({ @@ -139,6 +139,7 @@ export const createChannelListQuery = ({ if (userFilledChannelListQuery) { Object.keys(userFilledChannelListQuery).forEach((key) => { + // @ts-ignore params[key] = userFilledChannelListQuery[key]; }); } @@ -159,7 +160,7 @@ type SetupChannelListParams = { onChannelSelect: (channel: ChannelListInitialStateType['currentChannel']) => void; userFilledChannelListQuery: GroupChannelListQueryParamsInternal; logger: LoggerInterface; - sortChannelList: (channels: GroupChannel[]) => GroupChannel[]; + sortChannelList?: (channels: GroupChannel[]) => GroupChannel[]; disableAutoSelect: boolean; markAsDeliveredScheduler: MarkAsDeliveredSchedulerType; disableMarkAsDelivered: boolean; @@ -270,8 +271,7 @@ export const pubSubHandler = (pubSub: SBUGlobalPubSub, channelListDispatcher: Re if (!pubSub) return subscriber; subscriber.set( topics.CREATE_CHANNEL, - pubSub.subscribe(topics.CREATE_CHANNEL, (msg: { channel: GroupChannel }) => { - const { channel } = msg; + pubSub.subscribe(topics.CREATE_CHANNEL, ({ channel }) => { channelListDispatcher({ type: channelActions.CREATE_CHANNEL, payload: channel, diff --git a/src/modules/ChannelSettings/components/ChannelProfile/index.tsx b/src/modules/ChannelSettings/components/ChannelProfile/index.tsx index d745612d8..44ec58bb9 100644 --- a/src/modules/ChannelSettings/components/ChannelProfile/index.tsx +++ b/src/modules/ChannelSettings/components/ChannelProfile/index.tsx @@ -25,10 +25,10 @@ const ChannelProfile: React.FC = () => { const isOnline = state?.config?.isOnline; const disabled = !isOnline; - const { channel } = channelSettingStore; + const channel = channelSettingStore?.channel; const getChannelName = () => { - if (channel?.name && channel?.name !== 'Group Channel') { + if (channel?.name && channel.name !== 'Group Channel') { return channel.name; } if (channel?.name === 'Group Channel' || !channel?.name) { diff --git a/src/modules/ChannelSettings/components/ChannelSettingsUI/index.tsx b/src/modules/ChannelSettings/components/ChannelSettingsUI/index.tsx index 3c7575135..c9d2233d1 100644 --- a/src/modules/ChannelSettings/components/ChannelSettingsUI/index.tsx +++ b/src/modules/ChannelSettings/components/ChannelSettingsUI/index.tsx @@ -28,7 +28,7 @@ export interface ChannelSettingsUIProps { const ChannelSettingsUI = (props: ChannelSettingsUIProps) => { const { - renderHeader = (p) => , + renderHeader = (p: ChannelSettingsHeaderProps) => , renderLeaveChannel, renderChannelProfile, renderModerationPanel, @@ -37,11 +37,12 @@ const ChannelSettingsUI = (props: ChannelSettingsUIProps) => { } = deleteNullish(props); const state = useSendbirdStateContext(); - const isOnline = state?.config?.isOnline; - const { stringSet } = useContext(LocalizationContext); const { channel, invalidChannel, onCloseClick, loading } = useChannelSettingsContext(); const [showLeaveChannelModal, setShowLeaveChannelModal] = useState(false); + const isOnline = state?.config?.isOnline; + const { stringSet } = useContext(LocalizationContext); + if (loading) { if (renderPlaceholderLoading) return renderPlaceholderLoading(); return ; diff --git a/src/modules/ChannelSettings/components/EditDetailsModal/index.tsx b/src/modules/ChannelSettings/components/EditDetailsModal/index.tsx index fadc3ab21..5c3a7433d 100644 --- a/src/modules/ChannelSettings/components/EditDetailsModal/index.tsx +++ b/src/modules/ChannelSettings/components/EditDetailsModal/index.tsx @@ -12,6 +12,7 @@ import Label, { LabelColors, LabelTypography } from '../../../../ui/Label'; import TextButton from '../../../../ui/TextButton'; import ChannelAvatar from '../../../../ui/ChannelAvatar/index'; import uuidv4 from '../../../../utils/uuid'; +import { FileCompat } from '@sendbird/chat'; export type EditDetailsProps = { onSubmit: () => void; @@ -37,11 +38,11 @@ const EditDetails: React.FC = (props: EditDetailsProps) => { const theme = state?.config?.theme; const logger = state?.config?.logger; - const inputRef = useRef(null); - const formRef = useRef(null); - const hiddenInputRef = useRef(null); - const [currentImg, setCurrentImg] = useState(null); - const [newFile, setNewFile] = useState(null); + const inputRef = useRef(null); + const formRef = useRef(null); + const hiddenInputRef = useRef(null); + const [currentImg, setCurrentImg] = useState(null); + const [newFile, setNewFile] = useState(null); const { stringSet } = useContext(LocalizationContext); return ( @@ -51,14 +52,14 @@ const EditDetails: React.FC = (props: EditDetailsProps) => { submitText={stringSet.BUTTON__SAVE} onCancel={onCancel} onSubmit={() => { - if (title !== '' && !inputRef.current.value) { - if (formRef.current.reportValidity) { // might not work in explorer + if (title !== '' && !inputRef.current?.value) { + if (formRef.current?.reportValidity) { // might not work in explorer formRef.current.reportValidity(); } return; } - const currentTitle = inputRef.current.value; + const currentTitle = inputRef.current?.value; const currentImg = newFile; logger.info('ChannelSettings: Channel information being updated', { currentTitle, @@ -66,7 +67,7 @@ const EditDetails: React.FC = (props: EditDetailsProps) => { }); if (onBeforeUpdateChannel) { logger.info('ChannelSettings: onBeforeUpdateChannel'); - const params = onBeforeUpdateChannel(currentTitle, currentImg, channel?.data); + const params = onBeforeUpdateChannel(currentTitle ?? '', currentImg, channel?.data); channel?.updateChannel(params).then((groupChannel) => { onChannelModified?.(groupChannel); setChannelUpdateId(uuidv4()); @@ -75,13 +76,13 @@ const EditDetails: React.FC = (props: EditDetailsProps) => { } else { logger.info('ChannelSettings: normal'); channel?.updateChannel({ - coverImage: currentImg, + coverImage: currentImg as FileCompat, name: currentTitle, data: channel?.data || '', }).then((groupChannel) => { logger.info('ChannelSettings: Channel information updated', groupChannel); onChannelModified?.(groupChannel); - setChannelUpdateId(uuidv4()); + setChannelUpdateId?.(uuidv4()); onSubmit(); }); } @@ -123,14 +124,18 @@ const EditDetails: React.FC = (props: EditDetailsProps) => { accept="image/gif, image/jpeg, image/png" style={{ display: 'none' }} onChange={(e) => { - setCurrentImg(URL.createObjectURL(e.target.files[0])); - setNewFile(e.target.files[0]); - hiddenInputRef.current.value = ''; + if (e.target.files) { + setCurrentImg(URL.createObjectURL(e.target.files[0])); + setNewFile(e.target.files[0]); + } + if (hiddenInputRef.current) { + hiddenInputRef.current.value = ''; + } }} /> hiddenInputRef.current.click()} + onClick={() => hiddenInputRef.current?.click()} disableUnderline >