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 e8c4af7ad..e8884e068 100644 --- a/src/lib/MediaQueryContext.tsx +++ b/src/lib/MediaQueryContext.tsx @@ -11,7 +11,7 @@ const MediaQueryContext = React.createContext({ export interface MediaQueryProviderProps { children: React.ReactElement; - mediaQueryBreakPoint?: string; + mediaQueryBreakPoint?: string | boolean; logger?: Logger; } diff --git a/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx b/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx index b9289a919..dc5fa8398 100644 --- a/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx +++ b/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx @@ -283,6 +283,7 @@ export default function ParentMessageInfoItem({ = T | null; +export type SpaceFromTriggerType = { + x: number, + y: number, + top?: number, + left?: number, + height?: number, +}; + export interface UserListQuery { hasNext?: boolean; next(): Promise>; diff --git a/src/ui/ContextMenu/EmojiListItems.tsx b/src/ui/ContextMenu/EmojiListItems.tsx index abfd97910..eb8d3cceb 100644 --- a/src/ui/ContextMenu/EmojiListItems.tsx +++ b/src/ui/ContextMenu/EmojiListItems.tsx @@ -2,7 +2,9 @@ import React, { ReactElement, ReactNode, RefObject, useEffect, useRef, useState import { createPortal } from 'react-dom'; import SortByRow from '../SortByRow'; +import { Nullable } from '../../types'; +const defaultParentRect = { x: 0, y: 0, left: 0, top: 0, height: 0 }; type SpaceFromTrigger = { x: number, y: number }; type ReactionStyle = { left: number, top: number }; export interface EmojiListItemsProps { @@ -13,15 +15,15 @@ export interface EmojiListItemsProps { spaceFromTrigger?: SpaceFromTrigger; } -const EmojiListItems = ({ +export const EmojiListItems = ({ children, parentRef, parentContainRef, spaceFromTrigger = { x: 0, y: 0 }, closeDropdown, -}: EmojiListItemsProps): ReactElement => { +}: EmojiListItemsProps): Nullable => { const [reactionStyle, setReactionStyle] = useState({ left: 0, top: 0 }); - const reactionRef = useRef(null); + const reactionRef: RefObject = useRef(null); /* showParent & hideParent */ useEffect(() => { @@ -52,7 +54,7 @@ const EmojiListItems = ({ useEffect(() => { const spaceFromTriggerX = spaceFromTrigger?.x || 0; const spaceFromTriggerY = spaceFromTrigger?.y || 0; - const parentRect = parentRef?.current?.getBoundingClientRect(); + const parentRect = parentRef?.current?.getBoundingClientRect() ?? defaultParentRect; const x = parentRect.x || parentRect.left; const y = parentRect.y || parentRect.top; const reactionStyle = { @@ -84,33 +86,37 @@ const EmojiListItems = ({ } }, []); - return ( - createPortal( - <> -
-
    - +
    +
      - {children} - -
    - , - document.getElementById('sendbird-emoji-list-portal'), - ) - ); + + {children} + +
+ , + rootElement, + ) + ); + } + return null; }; export default EmojiListItems; diff --git a/src/ui/ContextMenu/MenuItems.tsx b/src/ui/ContextMenu/MenuItems.tsx index 471773665..cdccfa612 100644 --- a/src/ui/ContextMenu/MenuItems.tsx +++ b/src/ui/ContextMenu/MenuItems.tsx @@ -5,7 +5,7 @@ interface MenuItemsProps { className?: string; style?: Record; openLeft?: boolean; - children: React.ReactElement | Array; + children: React.ReactElement | Array | React.ReactNode; parentRef: React.RefObject; parentContainRef: React.RefObject; closeDropdown: () => void; diff --git a/src/ui/EmojiReactions/AddReactionBadgeItem.tsx b/src/ui/EmojiReactions/AddReactionBadgeItem.tsx new file mode 100644 index 000000000..2136b0bd1 --- /dev/null +++ b/src/ui/EmojiReactions/AddReactionBadgeItem.tsx @@ -0,0 +1,42 @@ +import React, { + KeyboardEvent, + MouseEvent, + TouchEvent, +} from 'react'; +import ReactionBadge from '../ReactionBadge'; +import Icon, { IconColors, IconTypes } from '../Icon'; +import useLongPress from '../../hooks/useLongPress'; + +export interface AddReactionBadgeItemProps { + onClick: (e: MouseEvent | KeyboardEvent | TouchEvent) => void; +} + +export const AddReactionBadgeItem = ({ + onClick, +}: AddReactionBadgeItemProps): React.ReactElement => { + const onlyClick = useLongPress({ + onLongPress: () => { /* noop */ }, + onClick, + }, { + shouldPreventDefault: true, + shouldStopPropagation: true, + }); + + return ( +
+ + + +
+ ); +}; diff --git a/src/ui/EmojiReactions/ReactionItem.tsx b/src/ui/EmojiReactions/ReactionItem.tsx new file mode 100644 index 000000000..4301818d2 --- /dev/null +++ b/src/ui/EmojiReactions/ReactionItem.tsx @@ -0,0 +1,89 @@ +import React, { useContext } from 'react'; + +import { FileMessage, Reaction, UserMessage } from '@sendbird/chat/message'; + +import Tooltip from '../Tooltip'; +import TooltipWrapper from '../TooltipWrapper'; +import ReactionBadge from '../ReactionBadge'; +import ImageRenderer from '../ImageRenderer'; +import Icon, { IconTypes } from '../Icon'; +import { useMediaQueryContext } from '../../lib/MediaQueryContext'; +import useLongPress from '../../hooks/useLongPress'; +import { LocalizationContext } from '../../lib/LocalizationContext'; +import useSendbirdStateContext from '../../hooks/useSendbirdStateContext'; +import { getEmojiTooltipString, isReactedBy } from '../../utils'; +import { useMessageContext } from '../../modules/Message/context/MessageProvider'; +import { Emoji } from '@sendbird/chat'; + +type Props = { + reaction: Reaction; + memberNicknamesMap: Map; + setEmojiKey: React.Dispatch>; + toggleReaction?: (message: UserMessage | FileMessage, key: string, byMe: boolean) => void; + emojisMap: Map; +}; + +export default function ReactionItem({ + reaction, + memberNicknamesMap, + setEmojiKey, + toggleReaction, + emojisMap, +}: Props) { + const store = useSendbirdStateContext(); + const { isMobile } = useMediaQueryContext(); + const messageStore = useMessageContext(); + const message = messageStore?.message as UserMessage; + const { stringSet } = useContext(LocalizationContext); + + const userId = store.config.userId; + const reactedByMe = isReactedBy(userId, reaction); + + const handleOnClick = () => { + setEmojiKey(''); + toggleReaction?.((message), reaction.key, reactedByMe); + }; + const longPress = useLongPress({ + onLongPress: () => { + setEmojiKey(reaction.key); + }, + onClick: handleOnClick, + }, { + shouldPreventDefault: true, + shouldStopPropagation: true, + }); + + return ( + 0) ? ( + + {getEmojiTooltipString(reaction, userId, memberNicknamesMap, stringSet)} + + ) : <>} + > +
+ + + )} + /> + +
+
+ ); +} diff --git a/src/ui/EmojiReactions/index.tsx b/src/ui/EmojiReactions/index.tsx index 8663b996f..ebdd5d144 100644 --- a/src/ui/EmojiReactions/index.tsx +++ b/src/ui/EmojiReactions/index.tsx @@ -1,85 +1,74 @@ import './index.scss'; -import React, { ReactElement, useContext, useRef } from 'react'; -import type { FileMessage, Reaction, UserMessage } from '@sendbird/chat/message'; +import React, { ReactElement, useRef, useState } from 'react'; import type { Emoji, EmojiContainer } from '@sendbird/chat'; +import type { FileMessage, Reaction, UserMessage } from '@sendbird/chat/message'; +import type { GroupChannel } from '@sendbird/chat/groupChannel'; -import Tooltip from '../Tooltip'; -import TooltipWrapper from '../TooltipWrapper'; import ReactionBadge from '../ReactionBadge'; import ReactionButton from '../ReactionButton'; import ImageRenderer from '../ImageRenderer'; import Icon, { IconTypes, IconColors } from '../Icon'; import ContextMenu, { EmojiListItems } from '../ContextMenu'; +import { Nullable, SpaceFromTriggerType } from '../../types'; -import { getClassName, getEmojiListAll, getEmojiMapAll, getEmojiTooltipString, isReactedBy } from '../../utils'; -import { LocalizationContext } from '../../lib/LocalizationContext'; +import { getClassName, getEmojiListAll, getEmojiMapAll } from '../../utils'; +import { ReactedMembersBottomSheet } from '../MobileMenu/ReactedMembersBottomSheet'; +import ReactionItem from './ReactionItem'; +import { useMediaQueryContext } from '../../lib/MediaQueryContext'; +import { AddReactionBadgeItem } from './AddReactionBadgeItem'; +import { MobileEmojisBottomSheet } from '../MobileMenu/MobileEmojisBottomSheet'; interface Props { className?: string | Array; userId: string; message: UserMessage | FileMessage; + channel: Nullable; emojiContainer: EmojiContainer; memberNicknamesMap: Map; - spaceFromTrigger?: { x: number, y: number }; + spaceFromTrigger?: SpaceFromTriggerType; isByMe?: boolean; toggleReaction?: (message: UserMessage | FileMessage, key: string, byMe: boolean) => void; } const EmojiReactions = ({ - className, + className = '', userId, message, + channel, emojiContainer, memberNicknamesMap, spaceFromTrigger = { x: 0, y: 0 }, isByMe = false, toggleReaction, }: Props): ReactElement => { - const { stringSet } = useContext(LocalizationContext); - const emojisMap = getEmojiMapAll(emojiContainer); + const { isMobile } = useMediaQueryContext(); const addReactionRef = useRef(null); + const [showEmojiList, setShowEmojiList] = useState(false); + const [selectedEmojiKey, setSelectedEmojiKey] = useState(''); + + const emojisMap = getEmojiMapAll(emojiContainer); + const showAddReactionBadge = (message.reactions?.length ?? 0) < emojisMap.size; return (
- {(message?.reactions?.length > 0) && ( - message.reactions.map((reaction: Reaction): ReactElement => { - const reactedByMe = isReactedBy(userId, reaction); + {((message.reactions?.length ?? 0) > 0) && ( + message.reactions?.map((reaction: Reaction): ReactElement => { return ( - 0) && ( - - {getEmojiTooltipString(reaction, userId, memberNicknamesMap, stringSet)} - - )} - > - { - toggleReaction(message, reaction.key, reactedByMe); - e?.stopPropagation?.(); - }} - > - - )} - /> - - + reaction={reaction} + memberNicknamesMap={memberNicknamesMap} + setEmojiKey={setSelectedEmojiKey} + toggleReaction={toggleReaction} + emojisMap={emojisMap} + /> ); }) )} - {(message?.reactions?.length < emojisMap.size) && ( + {(!isMobile && showAddReactionBadge) && ( void): ReactElement => ( {getEmojiListAll(emojiContainer).map((emoji: Emoji): ReactElement => { const isReacted: boolean = (message?.reactions ?.find((reaction: Reaction): boolean => reaction.key === emoji.key)?.userIds - ?.some((reactorId: string): boolean => reactorId === userId)); + ?.some((reactorId: string): boolean => reactorId === userId)) || false; return ( { closeDropdown(); - toggleReaction(message, emoji.key, isReacted); + toggleReaction?.(message, emoji.key, isReacted); e?.stopPropagation(); }} > @@ -144,6 +133,35 @@ const EmojiReactions = ({ )} /> )} + {(isMobile && showAddReactionBadge) && ( + { + setShowEmojiList(true); + }} + /> + )} + {(isMobile && showEmojiList) && ( + { + setShowEmojiList(false); + }} + toggleReaction={toggleReaction} + /> + )} + {(isMobile && selectedEmojiKey && channel !== null) && ( + { + setSelectedEmojiKey(''); + }} + emojiContainer={emojiContainer} + /> + )}
); }; diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 1d5a9daf8..812898ed4 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -154,6 +154,7 @@ export default function MessageContent({ }, }, { delay: 300, + shouldPreventDefault: false, }); if (message?.isAdminMessage?.() || message?.messageType === 'admin') { @@ -364,6 +365,7 @@ export default function MessageContent({ ; message: UserMessage | FileMessage; userId: string; - spaceFromTrigger?: Record; + spaceFromTrigger?: SpaceFromTriggerType; emojiContainer?: EmojiContainer; toggleReaction?: (message: UserMessage | FileMessage, reactionKey: string, isReacted: boolean) => void; setSupposedHover?: (bool: boolean) => void; @@ -24,7 +25,7 @@ export default function MessageItemReactionMenu({ className, message, userId, - spaceFromTrigger = {}, + spaceFromTrigger = { x: 0, y: 0 }, emojiContainer, toggleReaction, setSupposedHover, diff --git a/src/ui/MobileMenu/MobileContextMenu.tsx b/src/ui/MobileMenu/MobileContextMenu.tsx index e2a7cf479..db8601212 100644 --- a/src/ui/MobileMenu/MobileContextMenu.tsx +++ b/src/ui/MobileMenu/MobileContextMenu.tsx @@ -12,12 +12,13 @@ import { isUserMessage, copyToClipboard, isFileMessage, + isParentMessage, } from '../../utils'; import { useLocalization } from '../../lib/LocalizationContext'; import Icon, { IconTypes, IconColors } from '../Icon'; import Label, { LabelTypography } from '../Label'; -const MobileContextMenu: React.FunctionComponent = (props: BaseMenuProps) => { +const MobileContextMenu: React.FunctionComponent = (props: BaseMenuProps): React.ReactElement => { const { hideMenu, channel, @@ -29,6 +30,8 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe showRemove, setQuoteMessage, parentRef, + onReplyInThread, + isOpenedFromThread = false, } = props; const isByMe = message?.sender?.userId === userId; const { stringSet } = useLocalization(); @@ -41,6 +44,10 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe && !isFailedMessage(message) && !isPendingMessage(message) && (channel?.isGroupChannel() && !(channel as GroupChannel)?.isBroadcast); + const showMenuItemThread: boolean = (replyType === 'THREAD') && !isOpenedFromThread + && !isFailedMessage(message) + && !isPendingMessage(message) + && channel?.isGroupChannel(); const fileMessage = message as FileMessage; return ( @@ -92,6 +99,25 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe /> )} + {showMenuItemThread && ( + { + hideMenu(); + onReplyInThread?.({ message }); + }} + > + + + + )} {showMenuItemEdit && ( void; + toggleReaction?: (message: UserMessage | FileMessage, key: string, byMe: boolean) => void; +} + +export const MobileEmojisBottomSheet = ({ + userId, + message, + emojiContainer, + hideMenu, + toggleReaction, +}: MobileEmojisBottomSheetProps): ReactElement => { + const emojiAllList = useMemo(() => { + return getEmojiListAll(emojiContainer); + }, [emojiContainer]); + return ( + +
+ {emojiAllList.map((emoji) => { + const isReacted: boolean = (message?.reactions + ?.find((reaction: Reaction): boolean => reaction.key === emoji.key)?.userIds + ?.some((reactorId: string): boolean => reactorId === userId)) ?? false; + return ( + { + e?.stopPropagation(); + toggleReaction?.(message, emoji.key, isReacted); + hideMenu(); + }} + > + ): ReactElement => ( +
+ +
+ )} + /> +
+ ); + })} +
+
+ ); +}; diff --git a/src/ui/MobileMenu/ReactedMembersBottomSheet.tsx b/src/ui/MobileMenu/ReactedMembersBottomSheet.tsx new file mode 100644 index 000000000..6b6fd3378 --- /dev/null +++ b/src/ui/MobileMenu/ReactedMembersBottomSheet.tsx @@ -0,0 +1,95 @@ +import React, { ReactElement, useState } from 'react'; +import { EmojiContainer } from '@sendbird/chat'; +import { GroupChannel, Member } from '@sendbird/chat/groupChannel'; +import { FileMessage, Reaction, UserMessage } from '@sendbird/chat/message'; + +import './mobile-menu-reacted-members.scss'; + +import BottomSheet from '../BottomSheet'; +import { getEmojiUrl } from '../../utils'; +import ImageRenderer from '../ImageRenderer'; +import Icon, { IconColors, IconTypes } from '../Icon'; +import Label, { LabelColors, LabelTypography } from '../Label'; +import UserListItem from '../UserListItem'; + +export interface ReactedMembersBottomSheetProps { + message: UserMessage | FileMessage; + channel: GroupChannel; + emojiKey: string; + hideMenu: () => void; + emojiContainer?: EmojiContainer; +} + +export const ReactedMembersBottomSheet = ({ + message, + channel, + emojiKey = '', + hideMenu, + emojiContainer, +}: ReactedMembersBottomSheetProps): ReactElement => { + const { members = [] } = channel; + const [selectedEmoji, setSelectedEmoji] = useState(emojiKey); + + return ( + +
+
+ {message.reactions?.map((reaction: Reaction): ReactElement => { + const emojiUrl = getEmojiUrl(emojiContainer, reaction.key); + return ( +
{ + setSelectedEmoji(reaction.key); + }} + > + ( +
+ +
+ )} + /> + +
+ ); + })} +
+
+ { // making a member list who reacted to the message with the `selectedEmoji` + ( + message.reactions?.find(reaction => reaction.key === selectedEmoji) + ?.userIds.map((userId) => members.find((member) => member.userId === userId)) + .filter((member) => member !== undefined) as Array + ) + .map((member) => ( + + )) + } +
+
+
+ ); +}; diff --git a/src/ui/MobileMenu/index.tsx b/src/ui/MobileMenu/index.tsx index e81353d00..922c9bb6f 100644 --- a/src/ui/MobileMenu/index.tsx +++ b/src/ui/MobileMenu/index.tsx @@ -63,6 +63,7 @@ const MobileMenu: React.FC = (props: MobileBottomSheetPr setQuoteMessage={setQuoteMessage} parentRef={parentRef} onReplyInThread={onReplyInThread} + isOpenedFromThread={isOpenedFromThread} /> ) } diff --git a/src/ui/MobileMenu/mobile-menu-reacted-members.scss b/src/ui/MobileMenu/mobile-menu-reacted-members.scss new file mode 100644 index 000000000..606bfc9ee --- /dev/null +++ b/src/ui/MobileMenu/mobile-menu-reacted-members.scss @@ -0,0 +1,37 @@ +.sendbird-message__bottomsheet__reacted-members { + position: relative; + padding: 0px 16px; + box-sizing: border-box; + width: 100%; + height: 46px; + overflow-x: scroll; + + display: inline-flex; + flex-direction: row; + justify-content: center; + gap: 16px; +} +.sendbird-message__bottomsheet__reacted-members__item { + position: relative; + height: 100%; + + display: inline-flex; + gap: 4px; + flex-direction: row; + align-items: center;; +} + +.sendbird-message__bottomsheet__reactor-list { + position: relative; + padding: 0px 16px; + width: 100%; + height: 216px; + box-sizing: border-box; + + display: inline-flex; + flex-direction: column; + overflow-y: scroll; +} +.sendbird-message__bottomsheet__reactor-list__item.sendbird-user-list-item { + border-bottom: 0px; +} diff --git a/src/ui/UserListItem/index.tsx b/src/ui/UserListItem/index.tsx index 000a2dc14..1d677def2 100644 --- a/src/ui/UserListItem/index.tsx +++ b/src/ui/UserListItem/index.tsx @@ -26,6 +26,7 @@ export interface UserListItemProps { parentRef?: MutableRefObject, }): ReactElement; onChange?(e: ChangeEvent): void; + avatarSize?: string; } export default function UserListItem({ @@ -39,6 +40,7 @@ export default function UserListItem({ currentUser, action, onChange, + avatarSize = '40px', }: UserListItemProps): ReactElement { const uniqueKey = user.userId; const actionRef = React.useRef(null); @@ -68,8 +70,8 @@ export default function UserListItem({ className="sendbird-user-list-item__avatar" ref={avatarRef} src={user.profileUrl} - width="40px" - height="40px" + width={avatarSize} + height={avatarSize} onClick={() => { if (!disableUserProfile) { toggleDropdown(); diff --git a/src/utils/consts.ts b/src/utils/consts.ts index bf7c5fd88..200a8e1e3 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -4,8 +4,8 @@ export const SCROLL_BUFFER = 10; export const VOICE_RECORDER_CLICK_BUFFER_TIME = 250; export const VOICE_RECORDER_DEFAULT_MIN = 1000; // 1 seconds export const VOICE_RECORDER_DEFAULT_MAX = 600000; // 10 minutes -export const VOICE_RECORDER_MIME_TYPE = 'audio/webm'; export const VOICE_RECORDER_AUDIO_BITS = 128000; +export const BROWSER_SUPPORT_MIME_TYPE_LIST = ['audio/webm', 'audio/mp4', 'audio/mpeg', 'audio/ogg']; // voice message play export const VOICE_PLAYER_PLAYBACK_BUFFER = 0.01; diff --git a/src/utils/index.ts b/src/utils/index.ts index 390028a33..07f504b48 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -320,6 +320,14 @@ export const getEmojiMapAll = (emojiContainer: EmojiContainer): Map ({ key }) => key === targetKey; +export const getEmojiUrl = (emojiContainer?: EmojiContainer, emojiKey?: string): string => { + const isFindingKey = findEmojiUrl(emojiKey ?? ''); + return emojiContainer?.emojiCategories + .find((category) => category.emojis.some(isFindingKey))?.emojis + .find(isFindingKey) + ?.url || ''; +}; export const getUserName = (user: User): string => (user?.friendName || user?.nickname || user?.userId); export const getSenderName = (message: UserMessage | FileMessage): string => (message?.sender && getUserName(message?.sender));