diff --git a/scripts/index_d_ts b/scripts/index_d_ts index 13fe969c4..1fa6c536d 100644 --- a/scripts/index_d_ts +++ b/scripts/index_d_ts @@ -1864,6 +1864,7 @@ declare module '@sendbird/uikit-react/ui/Dropdown' { declare module '@sendbird/uikit-react/ui/EmojiReactions' { import type { EmojiCategory } from '@sendbird/chat'; import type { FileMessage, UserMessage } from '@sendbird/chat/message'; + import type { GroupChannel } from '@sendbird/chat/groupChannel'; interface EmojiContainer { emojiCategories: Array; @@ -1873,6 +1874,7 @@ declare module '@sendbird/uikit-react/ui/EmojiReactions' { className?: string | Array; userId: string; message: UserMessage | FileMessage; + channel: GroupChannel; emojiContainer: EmojiContainer; memberNicknamesMap: Map; spaceFromTrigger?: Record; diff --git a/src/hooks/VoiceRecorder/index.tsx b/src/hooks/VoiceRecorder/index.tsx index f3c646c8b..437396f32 100644 --- a/src/hooks/VoiceRecorder/index.tsx +++ b/src/hooks/VoiceRecorder/index.tsx @@ -1,9 +1,9 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { + BROWSER_SUPPORT_MIME_TYPE_LIST, VOICE_MESSAGE_FILE_NAME, VOICE_MESSAGE_MIME_TYPE, VOICE_RECORDER_AUDIO_BITS, - VOICE_RECORDER_MIME_TYPE, } from '../../utils/consts'; import useSendbirdStateContext from '../useSendbirdStateContext'; @@ -37,6 +37,11 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle const [mediaRecorder, setMediaRecorder] = useState(null); const [isRecordable, setIsRecordable] = useState(false); + const browserSupportMimeType = BROWSER_SUPPORT_MIME_TYPE_LIST.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''; + if (!browserSupportMimeType) { + logger.error('VoiceRecorder: Browser does not support mimeType', { mimmeTypes: BROWSER_SUPPORT_MIME_TYPE_LIST }); + } + const [webAudioUtils, setWebAudioUtils] = useState(null); useEffect(() => { if (isVoiceMessageEnabled && !webAudioUtils) { @@ -62,7 +67,7 @@ export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactEle logger.info('VoiceRecorder: Succeeded getting media stream.', stream); setIsRecordable(true); const mediaRecorder = new MediaRecorder(stream, { - mimeType: VOICE_RECORDER_MIME_TYPE, + mimeType: browserSupportMimeType, audioBitsPerSecond: VOICE_RECORDER_AUDIO_BITS, }); mediaRecorder.ondataavailable = (e) => { // when recording stops diff --git a/src/hooks/useLongPress.tsx b/src/hooks/useLongPress.tsx index 9e0664173..66fda22b5 100644 --- a/src/hooks/useLongPress.tsx +++ b/src/hooks/useLongPress.tsx @@ -50,6 +50,7 @@ interface PressHandlers { interface Options { delay?: number; shouldPreventDefault?: boolean; + shouldStopPropagation?: boolean; } interface UseLongPressType { @@ -67,6 +68,7 @@ export default function useLongPress({ }: PressHandlers, { delay = DEFAULT_DURATION, shouldPreventDefault = true, + shouldStopPropagation = false, }: Options = {}): UseLongPressType { const { isMobile } = useMediaQueryContext(); const [longPressTriggered, setLongPressTriggered] = useState(false); @@ -81,6 +83,9 @@ export default function useLongPress({ ...e, }; setDragTriggered(false); + if (shouldStopPropagation) { + e.stopPropagation(); + } if (shouldPreventDefault && e.target) { e.target.addEventListener( 'touchend', @@ -96,7 +101,7 @@ export default function useLongPress({ onLongPress(clonedEvent); setLongPressTriggered(true); }, delay); - }, [onLongPress, delay, shouldPreventDefault, isMobile]); + }, [onLongPress, delay, shouldPreventDefault, shouldStopPropagation, isMobile]); const clear = useCallback(( e: React.MouseEvent | React.TouchEvent, diff --git a/src/lib/MediaQueryContext.tsx b/src/lib/MediaQueryContext.tsx index af7987727..98c1506f6 100644 --- a/src/lib/MediaQueryContext.tsx +++ b/src/lib/MediaQueryContext.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; import type { Logger } from './SendbirdState'; -const DEFAULT_MOBILE = '0px'; +const DEFAULT_MOBILE = false; +// const DEFAULT_MOBILE = '768px'; const MOBILE_CLASSNAME = 'sendbird--mobile-mode'; const MediaQueryContext = React.createContext({ @@ -18,7 +19,7 @@ export interface MediaQueryProviderProps { const addClassNameToBody = () => { try { const body = document.querySelector('body'); - body.classList.add(MOBILE_CLASSNAME); + body?.classList.add(MOBILE_CLASSNAME); } catch { // noop } @@ -27,7 +28,7 @@ const addClassNameToBody = () => { const removeClassNameFromBody = () => { try { const body = document.querySelector('body'); - body.classList.remove(MOBILE_CLASSNAME); + body?.classList.remove(MOBILE_CLASSNAME); } catch { // noop } @@ -67,10 +68,10 @@ const MediaQueryProvider = (props: MediaQueryProviderProps): React.ReactElement }; updateSize(); window.addEventListener('resize', updateSize); - logger?.info?.('MediaQueryProvider: addEventListener', updateSize); + logger?.info?.('MediaQueryProvider: addEventListener', { updateSize }); return () => { window.removeEventListener('resize', updateSize); - logger?.info?.('MediaQueryProvider: removeEventListener', updateSize); + logger?.info?.('MediaQueryProvider: removeEventListener', { updateSize }); }; }, [mediaQueryBreakPoint]); return ( diff --git a/src/modules/App/AppLayout.tsx b/src/modules/App/AppLayout.tsx index 64fc09877..99424f1d2 100644 --- a/src/modules/App/AppLayout.tsx +++ b/src/modules/App/AppLayout.tsx @@ -22,11 +22,11 @@ export const AppLayout: React.FC = ( setCurrentChannel, } = props; const [showThread, setShowThread] = useState(false); - const [threadTargetMessage, setThreadTargetMessage] = useState(null); + const [threadTargetMessage, setThreadTargetMessage] = useState(null); const [showSettings, setShowSettings] = useState(false); const [showSearch, setShowSearch] = useState(false); - const [highlightedMessage, setHighlightedMessage] = useState(null); - const [startingPoint, setStartingPoint] = useState(null); + const [highlightedMessage, setHighlightedMessage] = useState(null); + const [startingPoint, setStartingPoint] = useState(null); const { isMobile } = useMediaQueryContext(); return ( <> @@ -46,6 +46,8 @@ export const AppLayout: React.FC = ( setHighlightedMessage={setHighlightedMessage} startingPoint={startingPoint} setStartingPoint={setStartingPoint} + threadTargetMessage={threadTargetMessage} + setThreadTargetMessage={setThreadTargetMessage} /> ) : ( diff --git a/src/modules/App/DesktopLayout.tsx b/src/modules/App/DesktopLayout.tsx index d17042e3f..d4f909cbe 100644 --- a/src/modules/App/DesktopLayout.tsx +++ b/src/modules/App/DesktopLayout.tsx @@ -34,7 +34,7 @@ export const DesktopLayout: React.FC = ( threadTargetMessage, setThreadTargetMessage, } = props; - const [animatedMessageId, setAnimatedMessageId] = useState(null); + const [animatedMessageId, setAnimatedMessageId] = useState(null); return (
@@ -44,8 +44,8 @@ export const DesktopLayout: React.FC = ( onProfileEditSuccess={onProfileEditSuccess} disableAutoSelect={disableAutoSelect} onChannelSelect={(channel) => { - setStartingPoint(null); - setHighlightedMessage(null); + setStartingPoint?.(null); + setHighlightedMessage?.(null); if (channel) { setCurrentChannel(channel); } else { @@ -93,7 +93,7 @@ export const DesktopLayout: React.FC = ( setAnimatedMessageId(null); }} onMessageHighlighted={() => { - setHighlightedMessage(null); + setHighlightedMessage?.(null); }} showSearchIcon={showSearchIcon} startingPoint={startingPoint} @@ -121,13 +121,13 @@ export const DesktopLayout: React.FC = ( channelUrl={currentChannel?.url || ''} onResultClick={(message) => { if (message.messageId === highlightedMessage) { - setHighlightedMessage(null); + setHighlightedMessage?.(null); setTimeout(() => { - setHighlightedMessage(message.messageId); + setHighlightedMessage?.(message.messageId); }); } else { - setStartingPoint(message.createdAt); - setHighlightedMessage(message.messageId); + setStartingPoint?.(message.createdAt); + setHighlightedMessage?.(message.messageId); } }} onCloseClick={() => { @@ -149,7 +149,7 @@ export const DesktopLayout: React.FC = ( setCurrentChannel(channel); } if (message?.messageId !== animatedMessageId) { - setStartingPoint(message?.createdAt); + setStartingPoint?.(message?.createdAt); } setTimeout(() => { setAnimatedMessageId(message?.messageId); diff --git a/src/modules/App/MobileLayout.tsx b/src/modules/App/MobileLayout.tsx index b91f06042..8531c2f3f 100644 --- a/src/modules/App/MobileLayout.tsx +++ b/src/modules/App/MobileLayout.tsx @@ -11,6 +11,7 @@ import ChannelList from '../ChannelList'; import Channel from '../Channel'; import ChannelSettings from '../ChannelSettings'; import MessageSearch from '../MessageSearch'; +import Thread from '../Thread'; import useSendbirdStateContext from '../../hooks/useSendbirdStateContext'; import uuidv4 from '../../utils/uuid'; @@ -19,6 +20,7 @@ enum PANELS { CHANNEL = 'CHANNEL', CHANNEL_SETTINGS = 'CHANNEL_SETTINGS', MESSAGE_SEARCH = 'MESSAGE_SEARCH', + THREAD = 'THREAD', } export const MobileLayout: React.FC = ( @@ -37,18 +39,21 @@ export const MobileLayout: React.FC = ( setHighlightedMessage, startingPoint, setStartingPoint, + threadTargetMessage, + setThreadTargetMessage, } = props; const [panel, setPanel] = useState(PANELS?.CHANNEL_LIST); + const [animatedMessageId, setAnimatedMessageId] = useState(null); const store = useSendbirdStateContext(); const sdk = store?.stores?.sdkStore?.sdk as SendbirdGroupChat; const userId = store?.config?.userId; - const goToMessage = (message?: BaseMessage | null) => { - setStartingPoint(message?.createdAt); + const goToMessage = (message?: BaseMessage | null, timeoutCb?: (msgId: number | null) => void) => { + setStartingPoint?.(message?.createdAt || null); setTimeout(() => { - setHighlightedMessage(message?.messageId); - }); + timeoutCb?.(message?.messageId || null); + }, 500); }; useEffect(() => { @@ -118,10 +123,23 @@ export const MobileLayout: React.FC = ( showSearchIcon={showSearchIcon} isMessageGroupingEnabled={isMessageGroupingEnabled} startingPoint={startingPoint} + animatedMessage={animatedMessageId} highlightedMessage={highlightedMessage} onChatHeaderActionClick={() => { setPanel(PANELS.CHANNEL_SETTINGS); }} + onReplyInThread={({ message }) => { + if (replyType === 'THREAD') { + setPanel(PANELS.THREAD); + setThreadTargetMessage(message); + } + }} + onQuoteMessageClick={({ message }) => { // thread message + if (replyType === 'THREAD') { + setThreadTargetMessage(message); + setPanel(PANELS.THREAD); + } + }} />
) @@ -151,7 +169,35 @@ export const MobileLayout: React.FC = ( }} onResultClick={(message) => { setPanel(PANELS.CHANNEL); - goToMessage(message); + goToMessage(message, (messageId) => { + setHighlightedMessage?.(messageId); + }); + }} + /> +
+ ) + } + { + panel === PANELS?.THREAD && ( +
+ { + setPanel(PANELS.CHANNEL); + }} + onMoveToParentMessage={({ message, channel }) => { + if (channel?.url !== currentChannel?.url) { + setPanel(PANELS.CHANNEL); + } + if (message?.messageId !== animatedMessageId) { + goToMessage(message, (messageId) => { + setAnimatedMessageId(messageId); + }); + } + setTimeout(() => { + setAnimatedMessageId(message?.messageId); + }, 500); }} />
diff --git a/src/modules/App/mobile.scss b/src/modules/App/mobile.scss index 99bbbc2df..e46553b57 100644 --- a/src/modules/App/mobile.scss +++ b/src/modules/App/mobile.scss @@ -6,3 +6,14 @@ .sb-show-main { padding: 0 !important; } + +.sb_mobile__panelwrap .sendbird-thread { + width: 100%; + height: 100%; + & .sendbird-thread-ui { + max-width: 100%; + & .sendbird-thread-ui__header { + width: 100%; + } + } +} diff --git a/src/modules/App/stories/integrated-app.scss b/src/modules/App/stories/integrated-app.scss index 3fea44195..47fc83639 100644 --- a/src/modules/App/stories/integrated-app.scss +++ b/src/modules/App/stories/integrated-app.scss @@ -20,6 +20,9 @@ height: 120px; justify-content: center; align-items: center; + @include mobile() { + height: 80px; + } } .sendbird-integrated-sample-app__moderations { @@ -27,8 +30,7 @@ padding: 18px; display: flex; flex-direction: column; - margin-left: calc((100% - 520px) / 2); - margin-right: calc((100% - 520px) / 2); + align-items: center; @include themed() { background-color: t(bg-1); } @@ -48,19 +50,49 @@ @include themed() { border-bottom: 1px solid t(on-bg-1); } + @include mobile() { + flex-direction: column; + flex-wrap: wrap; + align-items: center; + } +} +.sendbird-integrated-sample-app__moderations__option--toggle { + @include mobile() { + flex-direction: row; + flex-wrap: wrap; + width: 100%; + justify-content: space-between + } + .sendbird-integrated-sample-app__moderations__option__name, + .sendbird-integrated-sample-app__moderations__option__input { + @include mobile() { + width: auto; + } + } + .sendbird-integrated-sample-app-toggle-button { + text-align: right; + } } .sendbird-integrated-sample-app__moderations__option__name { width: 140px; + @include mobile() { + width: 100%; + } } .sendbird-integrated-sample-app__moderations__option__input { margin-left: 20px; + @include mobile() { + width: 100%; + margin-left: 0px; + } } .sendbird-integrated-app-submit-area { display: flex; width: 100%; justify-content: center; - gap: 10px + gap: 10px; + margin: 16px 0px; } // text input diff --git a/src/modules/App/stories/integrated.stories.js b/src/modules/App/stories/integrated.stories.js index e8e3e1643..efa1c3774 100644 --- a/src/modules/App/stories/integrated.stories.js +++ b/src/modules/App/stories/integrated.stories.js @@ -8,6 +8,7 @@ import Streamnig from '../../OpenChannelApp/Streaming'; import Label, { LabelTypography, LabelColors } from '../../../ui/Label'; import Icon, { IconTypes, IconColors } from '../../../ui/Icon'; import Button, { ButtonSizes, ButtonTypes } from '../../../ui/Button'; +import { MediaQueryProvider } from '../../../lib/MediaQueryContext'; const STORAGE_KEY = 'sendbird-integrated-app-v1-groupchannel'; @@ -45,12 +46,22 @@ const ChannelType = { LIVE: 'LIVE', }; +const TYPES = { + TOGGLE: 'TOGGLE', +}; + const ModerationOptionItem = ({ subTitle, children, + type, }) => { return ( -
+