From 681c1f703eb7357e53e949a9da5a6b56642d15f5 Mon Sep 17 00:00:00 2001 From: piotr-suwala <112620304+piotr-suwala@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:39:13 +0200 Subject: [PATCH] feat(lib): add channel suggestion box (#125) * feat(lib): add channel suggestion box * feat(lib): fix switching channels and channel id for direct conversations * feat(lib): rm a comment --- lib/src/entities/chat.ts | 3 +- lib/src/hash.ts | 15 +++ samples/react-native-group-chat/App.tsx | 10 +- .../DataSuggestionBox.tsx} | 18 ++-- .../components/data-suggestion-box/index.ts | 1 + .../components/index.ts | 2 +- .../components/message-text/MessageText.tsx | 34 ++++++- .../components/user-suggestion-box/index.ts | 1 - samples/react-native-group-chat/context.ts | 2 +- .../hooks/useCommonChatRenderers.tsx | 57 ++++++------ .../screens/ordinary/chat/Chat.tsx | 93 +++++++------------ .../new-chat-screen/NewChatScreen.tsx | 5 +- .../ordinary/thread-reply/ThreadReply.tsx | 31 ++++--- .../screens/tabs/home/HomeScreen.tsx | 1 + samples/react-native-group-chat/utils.ts | 3 +- 15 files changed, 159 insertions(+), 117 deletions(-) create mode 100644 lib/src/hash.ts rename samples/react-native-group-chat/components/{user-suggestion-box/UserSuggestionBox.tsx => data-suggestion-box/DataSuggestionBox.tsx} (57%) create mode 100644 samples/react-native-group-chat/components/data-suggestion-box/index.ts delete mode 100644 samples/react-native-group-chat/components/user-suggestion-box/index.ts diff --git a/lib/src/entities/chat.ts b/lib/src/entities/chat.ts index 75dc3b12..4b8c6bda 100644 --- a/lib/src/entities/chat.ts +++ b/lib/src/entities/chat.ts @@ -18,6 +18,7 @@ import { MESSAGE_THREAD_ID_PREFIX } from "../constants" import { ThreadChannel } from "./thread-channel" import { MentionsUtils } from "../mentions-utils" import { getErrorProxiedEntity, ErrorLogger } from "../error-logging" +import { cyrb53a } from "../hash" type ChatConfig = { saveDebugLog: boolean @@ -690,7 +691,7 @@ export class Chat { const sortedUsers = [this.user.id, user.id].sort() - const channelId = `direct.${sortedUsers[0]}&${sortedUsers[1]}` + const channelId = `direct.${cyrb53a(`${sortedUsers[0]}&${sortedUsers[1]}`)}` const channel = (await this.getChannel(channelId)) || diff --git a/lib/src/hash.ts b/lib/src/hash.ts new file mode 100644 index 00000000..5ce1fcb2 --- /dev/null +++ b/lib/src/hash.ts @@ -0,0 +1,15 @@ +export const cyrb53a = function (str: string, seed = 0) { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i) + h1 = Math.imul(h1 ^ ch, 0x85ebca77) + h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d) + } + h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97) + h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9) + h1 ^= h2 >>> 16 + h2 ^= h1 >>> 16 + + return 2097152 * (h2 >>> 0) + (h1 >>> 11) +} diff --git a/samples/react-native-group-chat/App.tsx b/samples/react-native-group-chat/App.tsx index 6227e72c..8c0da957 100644 --- a/samples/react-native-group-chat/App.tsx +++ b/samples/react-native-group-chat/App.tsx @@ -119,11 +119,17 @@ function App() { const [chat, setChat] = useState(null) const [users, setUsers] = useState([]) const [loading, setLoading] = useState(false) - const [currentChannel, setCurrentChannel] = useState() + const [currentChannel, setCurrentChannel] = useState() const [currentChannelMembers, setCurrentChannelMembers] = useState([]) const [userMemberships, setUserMemberships] = useState([]) - async function setCurrentChannelWithMembers(channel: Channel) { + async function setCurrentChannelWithMembers(channel: Channel | null) { + if (!channel) { + setCurrentChannelMembers([]) + setCurrentChannel(null) + return + } + const { members } = await channel.getMembers() setCurrentChannelMembers(members) setCurrentChannel(channel) diff --git a/samples/react-native-group-chat/components/user-suggestion-box/UserSuggestionBox.tsx b/samples/react-native-group-chat/components/data-suggestion-box/DataSuggestionBox.tsx similarity index 57% rename from samples/react-native-group-chat/components/user-suggestion-box/UserSuggestionBox.tsx rename to samples/react-native-group-chat/components/data-suggestion-box/DataSuggestionBox.tsx index 483ac90f..711a2e65 100644 --- a/samples/react-native-group-chat/components/user-suggestion-box/UserSuggestionBox.tsx +++ b/samples/react-native-group-chat/components/data-suggestion-box/DataSuggestionBox.tsx @@ -1,19 +1,23 @@ import React from "react" import { View, StyleSheet } from "react-native" import { ListItem } from "../list-item" -import { User } from "@pubnub/chat" import { colorPalette } from "../../ui-components" +import { Channel, User } from "@pubnub/chat" -type UserSuggestionBoxProps = { - users: User[] - onUserSelect: (user: User) => void +type DataSuggestionBoxProps = { + data: Channel[] | User[] + onSelect: (element: Channel | User) => void } -export function UserSuggestionBox({ users, onUserSelect }: UserSuggestionBoxProps) { +export function DataSuggestionBox({ data, onSelect }: DataSuggestionBoxProps) { return ( - {users.map((user) => ( - onUserSelect(user)} /> + {data.map((element) => ( + onSelect(element)} + /> ))} ) diff --git a/samples/react-native-group-chat/components/data-suggestion-box/index.ts b/samples/react-native-group-chat/components/data-suggestion-box/index.ts new file mode 100644 index 00000000..adcee1cf --- /dev/null +++ b/samples/react-native-group-chat/components/data-suggestion-box/index.ts @@ -0,0 +1 @@ +export * from "./DataSuggestionBox" diff --git a/samples/react-native-group-chat/components/index.ts b/samples/react-native-group-chat/components/index.ts index e7819e36..d9ee59f8 100644 --- a/samples/react-native-group-chat/components/index.ts +++ b/samples/react-native-group-chat/components/index.ts @@ -1,5 +1,5 @@ export * from "./actions-menu" export * from "./list-item" -export * from "./user-suggestion-box" +export * from "./data-suggestion-box" export * from "./quote" export * from "./avatar" diff --git a/samples/react-native-group-chat/components/message-text/MessageText.tsx b/samples/react-native-group-chat/components/message-text/MessageText.tsx index 92a7870e..c1794426 100644 --- a/samples/react-native-group-chat/components/message-text/MessageText.tsx +++ b/samples/react-native-group-chat/components/message-text/MessageText.tsx @@ -6,6 +6,7 @@ import { Quote } from "../quote" import { Text } from "../../ui-components" import { Message, MixedTextTypedElement } from "@pubnub/chat" import { ChatContext } from "../../context" +import { useNavigation } from "@react-navigation/native" type MessageTextProps = { onGoToMessage: (message: Message) => void @@ -13,12 +14,25 @@ type MessageTextProps = { } export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { - const { chat } = useContext(ChatContext) + const { chat, setCurrentChannel } = useContext(ChatContext) + const navigation = useNavigation() const openLink = (link: string) => { Linking.openURL(link) } + async function openChannel(channelId: string) { + if (!chat) return + const channel = await chat.getChannel(channelId) + if (!channel) { + alert("This channel no longer exists.") + return + } + navigation.pop() + setCurrentChannel(channel) + navigation.navigate("Chat") + } + const renderMessagePart = useCallback( (messagePart: MixedTextTypedElement, index: number, userId: string | number) => { // TODO make it look nice @@ -31,14 +45,24 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { } if (messagePart.type === "plainLink") { return ( - openLink(messagePart.content.link)}> + openLink(messagePart.content.link)} + color="sky150" + > {messagePart.content.link} ) } if (messagePart.type === "textLink") { return ( - openLink(messagePart.content.link)}> + openLink(messagePart.content.link)} + color="sky150" + > {messagePart.content.text} ) @@ -49,6 +73,7 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { key={index} variant="body" onPress={() => openLink(`https://pubnub.com/${messagePart.content.id}`)} + color="sky150" > @{messagePart.content.name} @@ -59,7 +84,8 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { openLink(`https://pubnub.com/${messagePart.content.id}`)} + onPress={() => openChannel(messagePart.content.id)} + color="sky150" > #{messagePart.content.name} diff --git a/samples/react-native-group-chat/components/user-suggestion-box/index.ts b/samples/react-native-group-chat/components/user-suggestion-box/index.ts deleted file mode 100644 index ed57d986..00000000 --- a/samples/react-native-group-chat/components/user-suggestion-box/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./UserSuggestionBox" diff --git a/samples/react-native-group-chat/context.ts b/samples/react-native-group-chat/context.ts index d4933ce3..4a232b8d 100644 --- a/samples/react-native-group-chat/context.ts +++ b/samples/react-native-group-chat/context.ts @@ -7,7 +7,7 @@ type ChatContextParams = { chat: null | Chat setChat: (chat: Chat | null) => void currentChannel?: Channel - setCurrentChannel: (channel: Channel) => void + setCurrentChannel: (channel: Channel | null) => void currentChannelMembers: Membership[] users: User[] setUsers: (users: User[]) => void diff --git a/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx b/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx index 72473f42..a4411433 100644 --- a/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx +++ b/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx @@ -1,48 +1,53 @@ import { FlatList, View, StyleSheet } from "react-native" -import React, { useCallback } from "react" -import { Message, MessageDraft, User } from "@pubnub/chat" +import React, { useCallback, useContext } from "react" +import { Channel, Message, MessageDraft, User } from "@pubnub/chat" import { Text } from "../ui-components" import { Bubble } from "react-native-gifted-chat" import { EnhancedIMessage } from "../utils" -import { Quote, UserSuggestionBox } from "../components" +import { Quote, DataSuggestionBox } from "../components" import { MessageText } from "../components/message-text" +import { ChatContext } from "../context" type UseCommonChatRenderersProps = { typingData: string[] - users: Map messageDraft: MessageDraft | null lastAffectedNameOccurrenceIndex: number setText: (text: string) => void - setShowSuggestedUsers: (value: boolean) => void - showSuggestedUsers: boolean + setShowSuggestedData: (value: boolean) => void + showSuggestedData: boolean giftedChatRef: React.RefObject> giftedChatMappedMessages: EnhancedIMessage[] - suggestedUsers: User[] + suggestedData: User[] | Channel[] } export function useCommonChatRenderers({ typingData, - users, messageDraft, lastAffectedNameOccurrenceIndex, setText, - setShowSuggestedUsers, + setShowSuggestedData, giftedChatRef, giftedChatMappedMessages, - suggestedUsers, - showSuggestedUsers, + suggestedData, + showSuggestedData, }: UseCommonChatRenderersProps) { - const handleUserToMention = useCallback( - (user: User) => { + const { getUser } = useContext(ChatContext) + + const handleSuggestionSelect = useCallback( + (suggestion: User | Channel) => { if (!messageDraft) { return } + if (suggestion instanceof User) { + messageDraft.addMentionedUser(suggestion, lastAffectedNameOccurrenceIndex) + } else { + messageDraft.addReferencedChannel(suggestion, lastAffectedNameOccurrenceIndex) + } - messageDraft.addMentionedUser(user, lastAffectedNameOccurrenceIndex) setText(messageDraft.value) - setShowSuggestedUsers(false) + setShowSuggestedData(false) }, - [messageDraft, lastAffectedNameOccurrenceIndex] + [messageDraft, lastAffectedNameOccurrenceIndex, setText, setShowSuggestedData] ) const scrollToMessage = useCallback( @@ -62,7 +67,7 @@ export function useCommonChatRenderers({ giftedChatRef.current.scrollToIndex({ animated: true, index: messageIndex }) }, - [giftedChatMappedMessages] + [giftedChatMappedMessages, giftedChatRef] ) const renderChatFooter = useCallback(() => { @@ -72,7 +77,7 @@ export function useCommonChatRenderers({ const quotedMessage = messageDraft.quotedMessage let quotedMessageComponent = null - let userSuggestionComponent = null + let dataSuggestionComponent = null if (quotedMessage) { quotedMessageComponent = ( @@ -85,19 +90,19 @@ export function useCommonChatRenderers({ ) } - if (showSuggestedUsers) { - userSuggestionComponent = ( - + if (showSuggestedData) { + dataSuggestionComponent = ( + ) } return ( <> {quotedMessageComponent} - {userSuggestionComponent} + {dataSuggestionComponent} ) - }, [messageDraft, showSuggestedUsers, scrollToMessage, suggestedUsers]) + }, [messageDraft, showSuggestedData, scrollToMessage, suggestedData, handleSuggestionSelect]) const renderFooter = useCallback(() => { if (!typingData.length) { @@ -107,7 +112,7 @@ export function useCommonChatRenderers({ if (typingData.length === 1) { return ( - {users.get(typingData[0])?.name || typingData[0]} is typing... + {getUser(typingData[0])?.name || typingData[0]} is typing... ) } @@ -115,12 +120,12 @@ export function useCommonChatRenderers({ return ( - {typingData.map((typingPoint) => users.get(typingPoint)?.name || typingPoint).join(", ")}{" "} + {typingData.map((typingPoint) => getUser(typingPoint)?.name || typingPoint).join(", ")}{" "} are typing... ) - }, [typingData, users]) + }, [getUser, typingData]) return { renderFooter, diff --git a/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx b/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx index f72bae95..4df41004 100644 --- a/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx +++ b/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect, useContext, useRef } from "rea import { StyleSheet, View, ActivityIndicator, TouchableOpacity, FlatList } from "react-native" import { GiftedChat, Bubble } from "react-native-gifted-chat" import { StackScreenProps } from "@react-navigation/stack" -import { User, MessageDraft, Message } from "@pubnub/chat" +import { User, MessageDraft, Message, Channel } from "@pubnub/chat" import { EnhancedIMessage, mapPNMessageToGChatMessage } from "../../../utils" import { ChatContext } from "../../../context" @@ -20,11 +20,10 @@ export function ChatScreen({}: StackScreenProps) { const [isMoreMessages, setIsMoreMessages] = useState(true) const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false) const [giftedChatMappedMessages, setGiftedChatMappedMessages] = useState([]) - const [users, setUsers] = useState(new Map()) const [typingData, setTypingData] = useState([]) const [messageDraft, setMessageDraft] = useState(null) - const [suggestedUsers, setSuggestedUsers] = useState([]) - const [showSuggestedUsers, setShowSuggestedUsers] = useState(false) + const [suggestedData, setSuggestedData] = useState([]) + const [showSuggestedData, setShowSuggestedData] = useState(false) const giftedChatRef = useRef>(null) const [lastAffectedNameOccurrenceIndex, setLastAffectedNameOccurrenceIndex] = useState(-1) const [text, setText] = useState("") @@ -33,15 +32,14 @@ export function ChatScreen({}: StackScreenProps) { ) const { renderFooter, renderMessageText, renderChatFooter } = useCommonChatRenderers({ typingData, - users, messageDraft, lastAffectedNameOccurrenceIndex, setText, giftedChatRef, giftedChatMappedMessages, - setShowSuggestedUsers, - showSuggestedUsers, - suggestedUsers, + setShowSuggestedData, + showSuggestedData, + suggestedData, }) const handleQuote = useCallback( @@ -76,39 +74,6 @@ export function ChatScreen({}: StackScreenProps) { onPinMessage: handlePin, }) - const updateUsersMap = useCallback((k: string, v: User | User[]) => { - if (Array.isArray(v)) { - const newUsers = new Map() - - v.forEach((user) => { - newUsers.set(user.id, { - ...user, - }) - }) - - setUsers(newUsers) - return - } - - setUsers(new Map(users.set(k, { ...v }))) - }, []) - - useEffect(() => { - async function init() { - if (!chat) { - return - } - - chat.getUsers({}).then((usersObject) => { - updateUsersMap("1", usersObject.users) - }) - - updateUsersMap(chat.currentUser.id, chat.currentUser) - } - - init() - }, [currentChannel]) - useEffect(() => { if (!giftedChatMappedMessages.length) { return @@ -118,13 +83,15 @@ export function ChatScreen({}: StackScreenProps) { giftedChatMappedMessages.map((giftedMessage) => giftedMessage.originalPnMessage), (newMessages) => { setGiftedChatMappedMessages( - newMessages.map((newMessage) => mapPNMessageToGChatMessage(newMessage, users)) + newMessages.map((newMessage) => + mapPNMessageToGChatMessage(newMessage, getUser(newMessage.userId)) + ) ) } ) return unstream - }, [giftedChatMappedMessages, users]) + }, [getUser, giftedChatMappedMessages]) const loadEarlierMessages = async () => { if (!currentChannel) { @@ -147,7 +114,7 @@ export function ChatScreen({}: StackScreenProps) { GiftedChat.prepend( giftedChatMappedMessages, historicalMessagesObject.messages - .map((msg) => mapPNMessageToGChatMessage(msg, users.get(msg.userId))) + .map((msg) => mapPNMessageToGChatMessage(msg, getUser(msg.userId))) .reverse() ) ) @@ -187,14 +154,22 @@ export function ChatScreen({}: StackScreenProps) { GiftedChat.prepend( [], historicalMessagesObject.messages - .map((msg) => mapPNMessageToGChatMessage(msg, users.get(msg.userId))) + .map((msg) => mapPNMessageToGChatMessage(msg, getUser(msg.userId))) + .reverse() + ) + ) + setGiftedChatMappedMessages((msgs) => + GiftedChat.prepend( + [], + historicalMessagesObject.messages + .map((msg) => mapPNMessageToGChatMessage(msg, getUser(msg.userId))) .reverse() ) ) } switchChannelImplementation() - }, [currentChannel, currentChannelMembership, users]) + }, [currentChannel, currentChannelMembership, getUser]) useEffect(() => { if (!currentChannel) { @@ -202,20 +177,12 @@ export function ChatScreen({}: StackScreenProps) { } const disconnect = currentChannel.connect((message) => { - if (!users.get(message.userId)) { - chat?.getUser(message.userId).then((newUser) => { - if (newUser) { - updateUsersMap(message.userId, newUser) - } - }) - } - if (currentChannelMembership) { currentChannelMembership.setLastReadMessage(message) } setGiftedChatMappedMessages((currentMessages) => GiftedChat.append(currentMessages, [ - mapPNMessageToGChatMessage(message, users.get(message.userId)), + mapPNMessageToGChatMessage(message, getUser(message.userId)), ]) ) }) @@ -223,7 +190,7 @@ export function ChatScreen({}: StackScreenProps) { return () => { disconnect() } - }, [currentChannel, users, currentChannelMembership]) + }, [currentChannel, currentChannelMembership, getUser]) const resetInput = () => { if (!messageDraft) { @@ -250,12 +217,20 @@ export function ChatScreen({}: StackScreenProps) { } messageDraft.onChange(text).then((suggestionObject) => { - setSuggestedUsers(suggestionObject.users.suggestedUsers) - setLastAffectedNameOccurrenceIndex(suggestionObject.users.nameOccurrenceIndex) + setSuggestedData( + suggestionObject.users.suggestedUsers.length + ? suggestionObject.users.suggestedUsers + : suggestionObject.channels.suggestedChannels + ) + setLastAffectedNameOccurrenceIndex( + suggestionObject.users.suggestedUsers.length + ? suggestionObject.users.nameOccurrenceIndex + : suggestionObject.channels.channelOccurrenceIndex + ) }) setText(messageDraft.value) - setShowSuggestedUsers(true) + setShowSuggestedData(true) }, [messageDraft, currentChannel] ) diff --git a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx index 16bf3356..7867e9ee 100644 --- a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx +++ b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx @@ -17,7 +17,10 @@ export function NewChatScreen({ navigation }: StackScreenProps>(null) const [typingData, setTypingData] = useState([]) const [isParentMessageCollapsed, setIsParentMessageCollapsed] = useState(false) - const [suggestedUsers, setSuggestedUsers] = useState([]) - const [showSuggestedUsers, setShowSuggestedUsers] = useState(false) + const [suggestedData, setSuggestedData] = useState([]) + const [showSuggestedData, setShowSuggestedData] = useState(false) const [lastAffectedNameOccurrenceIndex, setLastAffectedNameOccurrenceIndex] = useState(-1) const { renderFooter, renderMessageText, renderChatFooter } = useCommonChatRenderers({ typingData, - users: new Map(), setText, messageDraft, - suggestedUsers, - showSuggestedUsers, - setShowSuggestedUsers, + suggestedData, + showSuggestedData, + setShowSuggestedData, giftedChatMappedMessages, giftedChatRef, lastAffectedNameOccurrenceIndex, @@ -92,7 +91,7 @@ export function ThreadReply({ route }: StackScreenProps { async function init() { @@ -130,7 +129,7 @@ export function ThreadReply({ route }: StackScreenProps { if (!currentThreadChannel) { @@ -173,12 +172,20 @@ export function ThreadReply({ route }: StackScreenProps { - setSuggestedUsers(suggestionObject.users.suggestedUsers) - setLastAffectedNameOccurrenceIndex(suggestionObject.users.nameOccurrenceIndex) + setSuggestedData( + suggestionObject.users.suggestedUsers.length + ? suggestionObject.users.suggestedUsers + : suggestionObject.channels.suggestedChannels + ) + setLastAffectedNameOccurrenceIndex( + suggestionObject.users.suggestedUsers.length + ? suggestionObject.users.nameOccurrenceIndex + : suggestionObject.channels.channelOccurrenceIndex + ) }) setText(messageDraft.value) - setShowSuggestedUsers(true) + setShowSuggestedData(true) }, [messageDraft] ) diff --git a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx index 15f182b2..9cd674bf 100644 --- a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx @@ -53,6 +53,7 @@ export function HomeScreen({ navigation }: StackScreenProps { fetchUnreadMessagesCount() + setCurrentChannel(null) }, []) ) diff --git a/samples/react-native-group-chat/utils.ts b/samples/react-native-group-chat/utils.ts index fc570351..3efc8b10 100644 --- a/samples/react-native-group-chat/utils.ts +++ b/samples/react-native-group-chat/utils.ts @@ -7,7 +7,7 @@ export type EnhancedIMessage = IMessage & { export function mapPNMessageToGChatMessage( pnMessage: Message, - user?: User & { thumbnail: string } + user?: User | null ): EnhancedIMessage { return { _id: pnMessage.timetoken, @@ -17,7 +17,6 @@ export function mapPNMessageToGChatMessage( user: { _id: user?.id || pnMessage.userId, name: user?.name || "Missing user name", - avatar: user?.thumbnail, }, } }