diff --git a/packages/messenger-widget/src/adapters/contacts.ts b/packages/messenger-widget/src/adapters/contacts.ts index e03a2bda8..e35b91c49 100644 --- a/packages/messenger-widget/src/adapters/contacts.ts +++ b/packages/messenger-widget/src/adapters/contacts.ts @@ -168,11 +168,7 @@ export async function addContact( userDb: UserDB, createEmptyConversationEntry: (id: string) => void, ) { - if ( - !createEmptyConversation(ensName, userDb, createEmptyConversationEntry) - ) { - throw Error('Contact exists already.'); - } + createEmptyConversation(ensName, userDb, createEmptyConversationEntry); } function fetchDeliveryServiceProfile(connection: Connection) { diff --git a/packages/messenger-widget/src/adapters/messages.ts b/packages/messenger-widget/src/adapters/messages.ts index 2beab6c8e..5e08097b8 100644 --- a/packages/messenger-widget/src/adapters/messages.ts +++ b/packages/messenger-widget/src/adapters/messages.ts @@ -8,11 +8,21 @@ import { buildEnvelop, EncryptionEnvelop, } from 'dm3-lib-messaging'; -import { getDeliveryServiceClient } from 'dm3-lib-profile'; +import { + Account, + getDeliveryServiceClient, + getDeliveryServiceProfile, + normalizeEnsName, +} from 'dm3-lib-profile'; import { log } from 'dm3-lib-shared'; -import { StorageEnvelopContainer } from 'dm3-lib-storage'; +import { + StorageEnvelopContainer, + UserDB, + getConversation, +} from 'dm3-lib-storage'; import { Connection } from '../interfaces/web3'; import { withAuthHeader } from './auth'; +import { decryptAsymmetric } from 'dm3-lib-crypto'; export async function fetchPendingConversations( connection: Connection, @@ -158,7 +168,137 @@ export async function sendMessage( ); } +export async function fetchAndStoreMessages( + connection: Connection, + deliveryServiceToken: string, + contact: string, + userDb: UserDB, + storeMessages: (envelops: StorageEnvelopContainer[]) => void, + contacts: Account[], +): Promise { + const profile = connection.account?.profile; + + if (!profile) { + throw Error('Account has no profile'); + } + //Fetch evey delivery service's profie + const deliveryServices = await Promise.all( + profile.deliveryServices.map(async (ds) => { + const deliveryServiceProfile = await getDeliveryServiceProfile( + ds, + connection.provider!, + async (url) => (await axios.get(url)).data, + ); + return deliveryServiceProfile?.url; + }), + ); + + //Filter every deliveryService without an url + const deliveryServiceUrls = deliveryServices.filter( + (ds): ds is string => !!ds, + ); + + //Fetch messages from each deliveryService + const messages = await Promise.all( + deliveryServiceUrls.map(async (baseUrl) => { + return await fetchNewMessages( + connection, + deliveryServiceToken, + contact, + ); + }), + ); + + //Flatten the message arrays of each delivery service to one message array + const allMessages = messages.reduce((agg, cur) => [...agg, ...cur], []); + const isFulfilled = ( + p: PromiseSettledResult, + ): p is PromiseFulfilledResult => p.status === 'fulfilled'; + + const envelops = ( + await Promise.allSettled( + /** + * Decrypts every message using the receivers encryptionKey + */ + allMessages.map( + async (envelop: any): Promise => { + const decryptedEnvelop = await decryptMessages( + [envelop], + userDb, + ); + + return { + envelop: decryptedEnvelop[0], + messageState: MessageState.Send, + deliveryServiceIncommingTimestamp: + decryptedEnvelop[0].postmark?.incommingTimestamp, + }; + }, + ), + ) + ) + .filter(isFulfilled) + .map((settledResult) => settledResult.value); + + //Storing the newly fetched messages in the userDb + storeMessages(envelops); + + try { + //Return all messages from the conversation between the user and their contact + return getConversation(contact, contacts, userDb); + } catch (error) { + return []; + } +} + +async function decryptMessages( + envelops: EncryptionEnvelop[], + userDb: UserDB, +): Promise { + return Promise.all( + envelops.map( + async (envelop): Promise => ({ + message: JSON.parse( + await decryptAsymmetric( + userDb.keys.encryptionKeyPair, + JSON.parse(envelop.message), + ), + ), + postmark: JSON.parse( + await decryptAsymmetric( + userDb.keys.encryptionKeyPair, + JSON.parse(envelop.postmark!), + ), + ), + metadata: envelop.metadata, + }), + ), + ); +} + +export async function fetchNewMessages( + connection: Connection, + token: string, + contactAddress: string, +): Promise { + const { account } = connection; + const deliveryPath = process.env.REACT_APP_BACKEND + '/delivery'; + const url = `${deliveryPath}/messages/${normalizeEnsName( + account!.ensName, + )}/contact/${contactAddress}`; + + const { data } = await getDeliveryServiceClient( + account!.profile!, + connection.provider!, + async (url: string) => (await axios.get(url)).data, + ).get(url, withAuthHeader(token)); + + return data; +} + export type SendMessage = typeof sendMessage; -export type CreatePendingEntry = typeof createPendingEntry; export type SubmitMessageType = typeof submitMessage; +export type GetNewMessages = typeof fetchNewMessages; +export type CreatePendingEntry = typeof createPendingEntry; +export type FetchAndStoreMessages = typeof fetchAndStoreMessages; export type FetchPendingConversations = typeof fetchPendingConversations; diff --git a/packages/messenger-widget/src/assets/images/emoji.svg b/packages/messenger-widget/src/assets/images/emoji.svg new file mode 100644 index 000000000..1b4a3b6e6 --- /dev/null +++ b/packages/messenger-widget/src/assets/images/emoji.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/messenger-widget/src/assets/images/file.svg b/packages/messenger-widget/src/assets/images/file.svg new file mode 100644 index 000000000..80925542c --- /dev/null +++ b/packages/messenger-widget/src/assets/images/file.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/messenger-widget/src/assets/images/send-btn.svg b/packages/messenger-widget/src/assets/images/send-btn.svg new file mode 100644 index 000000000..512fa5dae --- /dev/null +++ b/packages/messenger-widget/src/assets/images/send-btn.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/messenger-widget/src/assets/images/tick.svg b/packages/messenger-widget/src/assets/images/tick.svg new file mode 100644 index 000000000..b3c4f0726 --- /dev/null +++ b/packages/messenger-widget/src/assets/images/tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/messenger-widget/src/components/Chat/Chat.css b/packages/messenger-widget/src/components/Chat/Chat.css index a81f897de..55692572f 100644 --- a/packages/messenger-widget/src/components/Chat/Chat.css +++ b/packages/messenger-widget/src/components/Chat/Chat.css @@ -5,3 +5,20 @@ .highlight-chat-border-none { border-top: none; } + +.chat-container { + min-height: 86vh; +} + +.chat-items { + overflow-y: scroll; + margin-top: 0.5rem; +} + +.chat-height-small { + height: 63vh; +} + +.chat-height-high { + height: 75vh; +} diff --git a/packages/messenger-widget/src/components/Chat/Chat.tsx b/packages/messenger-widget/src/components/Chat/Chat.tsx index 40cf46072..5d89c5186 100644 --- a/packages/messenger-widget/src/components/Chat/Chat.tsx +++ b/packages/messenger-widget/src/components/Chat/Chat.tsx @@ -1,10 +1,80 @@ import './Chat.css'; -import { useContext } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { GlobalContext } from '../../utils/context-utils'; import ConfigProfileAlertBox from '../ContactProfileAlertBox/ContactProfileAlertBox'; +import { Message } from '../Message/Message'; +import { MessageProps } from '../../interfaces/props'; +import { MessageInput } from '../MessageInput/MessageInput'; +import { getConversation } from 'dm3-lib-storage'; +import { globalConfig, log } from 'dm3-lib-shared'; +import { + checkUserProfileConfigured, + getPastMessages, + handleMessages, + scrollToBottomOfChat, +} from './bl'; export function Chat() { - const { state } = useContext(GlobalContext); + const { state, dispatch } = useContext(GlobalContext); + + const [messageList, setMessageList] = useState([]); + const [isProfileConfigured, setIsProfileConfigured] = + useState(false); + + const alias = + state.connection.ethAddress && + state.connection.ethAddress + globalConfig.ADDR_ENS_SUBDOMAIN(); + + const setProfileCheck = (status: boolean) => { + setIsProfileConfigured(status); + }; + + const setListOfMessages = (msgs: []) => { + setMessageList(msgs); + }; + + useEffect(() => { + checkUserProfileConfigured( + state, + state.accounts.selectedContact?.account.ensName as string, + setProfileCheck, + ); + if ( + state.accounts.selectedContact && + state.userDb && + state.accounts.contacts + ) { + try { + handleMessages( + state, + dispatch, + getConversation( + state.accounts.selectedContact.account.ensName, + state.accounts.contacts.map( + (contact) => contact.account, + ), + state.userDb, + ), + alias, + setListOfMessages, + ); + } catch (error) { + log(error, 'error'); + } + } + }, [ + state.userDb?.conversations, + state.accounts.selectedContact, + state.accounts.contacts, + ]); + + useEffect(() => { + getPastMessages(state, dispatch, alias, setListOfMessages); + }, [state.accounts.selectedContact]); + + useEffect(() => { + scrollToBottomOfChat(); + }, [messageList]); return (
- {/* To show information box that contact has not created profile */} - {!state.accounts.selectedContact?.account.profile - ?.publicEncryptionKey && } +
+ {/* To show information box that contact has not created profile */} + {!isProfileConfigured && } + + {/* Chat messages */} +
+ {messageList.length > 0 && + messageList.map((messageData, index) => ( +
+ +
+ ))} +
+
-
Chat screen...
+ {/* Message, emoji and file attachments */} + +
); } diff --git a/packages/messenger-widget/src/components/Chat/bl.ts b/packages/messenger-widget/src/components/Chat/bl.ts new file mode 100644 index 000000000..ef76b0abf --- /dev/null +++ b/packages/messenger-widget/src/components/Chat/bl.ts @@ -0,0 +1,197 @@ +import { checkSignature as _checkSignature } from 'dm3-lib-crypto'; +import { Message, MessageState } from 'dm3-lib-messaging'; +import { + getUserProfile, + isSameEnsName, + normalizeEnsName, +} from 'dm3-lib-profile'; +import { log, stringify } from 'dm3-lib-shared'; +import { Actions, GlobalState, UserDbType } from '../../utils/enum-type-utils'; +import { StorageEnvelopContainer, UserDB } from 'dm3-lib-storage'; +import { fetchAndStoreMessages } from '../../adapters/messages'; +import { MessageProps } from '../../interfaces/props'; + +// method to check message signature +export async function checkSignature( + message: Message, + publicSigningKey: string, + ensName: string, + signature: string, +): Promise { + const sigCheck = await _checkSignature( + publicSigningKey, + stringify(message)!, + signature, + ); + + if ( + sigCheck && + normalizeEnsName(ensName) !== normalizeEnsName(message.metadata.from) + ) { + return true; + } else { + log(`Signature check for ${ensName} failed.`, 'error'); + return false; + } +} + +// method to check user profile is configured or not +export const checkUserProfileConfigured = async ( + state: GlobalState, + ensName: string, + setProfileCheck: Function, +) => { + try { + const profileDetails = await getUserProfile( + state.connection.provider!, + ensName, + ); + setProfileCheck( + profileDetails && profileDetails.profile.publicEncryptionKey + ? true + : false, + ); + } catch (error) { + setProfileCheck(false); + } +}; + +// method to scroll down to latest message automatically +export const scrollToBottomOfChat = () => { + const element: HTMLElement = document.getElementById( + 'chat-box', + ) as HTMLElement; + if (element) element.scrollTop = element.scrollHeight; +}; + +// method to set message format +const handleMessageContainer = ( + state: GlobalState, + messageContainers: StorageEnvelopContainer[], + alias: string | undefined, + setListOfMessages: Function, +) => { + const msgList: any = []; + let msg: MessageProps; + messageContainers.forEach((container: StorageEnvelopContainer) => { + msg = { + message: container.envelop.message.message!, + time: container.envelop.message.metadata.timestamp.toString(), + messageState: container.messageState, + ownMessage: false, + }; + if ( + isSameEnsName( + container.envelop.message.metadata.from, + state.connection.account!.ensName, + alias, + ) + ) { + msg.ownMessage = true; + } + + msgList.push(msg); + }); + setListOfMessages(msgList); +}; + +// method to set the message list +export const handleMessages = ( + state: GlobalState, + dispatch: React.Dispatch, + containers: StorageEnvelopContainer[], + alias: string | undefined, + setListOfMessages: Function, +): void => { + const checkedContainers = containers.filter((container) => { + if (!state.accounts.selectedContact) { + throw Error('No selected contact'); + } + + const account = isSameEnsName( + container.envelop.message.metadata.from, + state.accounts.selectedContact.account.ensName, + alias, + ) + ? state.accounts.selectedContact.account + : state.connection.account!; + + return account.profile?.publicSigningKey + ? checkSignature( + container.envelop.message, + account.profile?.publicSigningKey, + account.ensName, + container.envelop.message.signature, + ) + : true; + }); + + const newMessages = checkedContainers + .filter((conatier) => conatier.messageState === MessageState.Send) + .map((container) => ({ + ...container, + messageState: MessageState.Read, + })); + + const oldMessages = checkedContainers.filter( + (conatier) => + conatier.messageState === MessageState.Read || + conatier.messageState === MessageState.Created, + ); + + handleMessageContainer(state, oldMessages, alias, setListOfMessages); + + if (!state.userDb) { + throw Error( + `[handleMessages] Couldn't handle new messages. User db not created.`, + ); + } + + if (newMessages.length > 0) { + newMessages.forEach((message) => + dispatch({ + type: UserDbType.addMessage, + payload: { + container: message, + connection: state.connection, + }, + }), + ); + } +}; + +// method to fetch old messages +export const getPastMessages = async ( + state: GlobalState, + dispatch: React.Dispatch, + alias: string | undefined, + setListOfMessages: Function, +) => { + if (!state.accounts.selectedContact) { + throw Error('no contact selected'); + } + const messages = await fetchAndStoreMessages( + state.connection, + state.auth.currentSession?.token!, + state.accounts.selectedContact.account.ensName, + state.userDb as UserDB, + (envelops) => { + envelops.forEach((envelop) => + dispatch({ + type: UserDbType.addMessage, + payload: { + container: envelop, + connection: state.connection, + }, + }), + ); + }, + state.accounts.contacts + ? state.accounts.contacts.map((contact) => contact.account) + : [], + ); + + if (messages.length > 0) { + handleMessages(state, dispatch, messages, alias, setListOfMessages); + } +}; diff --git a/packages/messenger-widget/src/components/ConfigureProfile/bl.ts b/packages/messenger-widget/src/components/ConfigureProfile/bl.ts index 5da4ae55b..fe66db698 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/bl.ts +++ b/packages/messenger-widget/src/components/ConfigureProfile/bl.ts @@ -1,8 +1,4 @@ -import { - formatAddress, - getUserProfile, - SignedUserProfile, -} from 'dm3-lib-profile'; +import { formatAddress, SignedUserProfile } from 'dm3-lib-profile'; import { Actions, ConnectionType, diff --git a/packages/messenger-widget/src/components/ConfigureProfileBox/ConfigureProfileBox.css b/packages/messenger-widget/src/components/ConfigureProfileBox/ConfigureProfileBox.css index 063a25ef3..097451f0c 100644 --- a/packages/messenger-widget/src/components/ConfigureProfileBox/ConfigureProfileBox.css +++ b/packages/messenger-widget/src/components/ConfigureProfileBox/ConfigureProfileBox.css @@ -1,7 +1,7 @@ .box-outer-container { padding: 10px 1px 10px 0px; bottom: 1%; - margin: 1rem 1rem 1rem 1rem; + margin: 2rem 1rem 1rem 1rem; } .configure-msg-box { diff --git a/packages/messenger-widget/src/components/ContactProfileAlertBox/ContactProfileAlertBox.tsx b/packages/messenger-widget/src/components/ContactProfileAlertBox/ContactProfileAlertBox.tsx index d1e86c349..dcfe86bf7 100644 --- a/packages/messenger-widget/src/components/ContactProfileAlertBox/ContactProfileAlertBox.tsx +++ b/packages/messenger-widget/src/components/ContactProfileAlertBox/ContactProfileAlertBox.tsx @@ -3,7 +3,7 @@ import '../../styles/common.css'; export default function ConfigProfileAlertBox() { return (
diff --git a/packages/messenger-widget/src/components/Contacts/Contacts.css b/packages/messenger-widget/src/components/Contacts/Contacts.css index 17650f83a..d7df292cb 100644 --- a/packages/messenger-widget/src/components/Contacts/Contacts.css +++ b/packages/messenger-widget/src/components/Contacts/Contacts.css @@ -21,8 +21,8 @@ } .pic { - height: 3.125rem; - width: 3.125rem; + height: 3rem; + width: 3rem; } .content { diff --git a/packages/messenger-widget/src/components/Contacts/Contacts.tsx b/packages/messenger-widget/src/components/Contacts/Contacts.tsx index a51ca990c..484fcde8e 100644 --- a/packages/messenger-widget/src/components/Contacts/Contacts.tsx +++ b/packages/messenger-widget/src/components/Contacts/Contacts.tsx @@ -68,11 +68,6 @@ export function Contacts(props: DashboardProps) { // handles changes in conversation useEffect(() => { if (state.userDb?.conversations && state.userDb?.conversationsCount) { - dispatch({ - type: ModalStateType.LoaderContent, - payload: 'Fetching contacts...', - }); - startLoader(); props.getContacts(state, dispatch, props.dm3Props.config); } }, [state.userDb?.conversations, state.userDb?.conversationsCount]); @@ -259,7 +254,7 @@ export function Contacts(props: DashboardProps) { )}
-
+

{data.message}

diff --git a/packages/messenger-widget/src/components/Message/Message.css b/packages/messenger-widget/src/components/Message/Message.css new file mode 100644 index 000000000..682f0b0de --- /dev/null +++ b/packages/messenger-widget/src/components/Message/Message.css @@ -0,0 +1,34 @@ +.content-style { + padding: 10px; + max-width: 38rem; + margin-right: 0.4rem; +} + +.action-container { + height: fit-content; + width: fit-content; + border: 1px solid var(--text-primary-color); +} + +.chat-action-dot { + height: 0.9rem; + width: 0.7rem; +} + +.time-style { + margin-top: 3px; + font-size: 10px; + margin-right: 0.3rem; +} + +.readed-tick-icon { + color: var(--normal-btn-hover); +} + +.tick-icon { + margin-left: 2px; +} + +.second-tick { + margin-left: -5px; +} diff --git a/packages/messenger-widget/src/components/Message/Message.tsx b/packages/messenger-widget/src/components/Message/Message.tsx new file mode 100644 index 000000000..92b1acde3 --- /dev/null +++ b/packages/messenger-widget/src/components/Message/Message.tsx @@ -0,0 +1,54 @@ +import './Message.css'; +import { MessageProps } from '../../interfaces/props'; +import tickIcon from '../../assets/images/tick.svg'; +import { MessageState } from 'dm3-lib-messaging'; + +export function Message(props: MessageProps) { + return ( + +
+
+ {props.message} +
+
+
+ {new Date(Number(props.time)).toLocaleString()} + + {!props.ownMessage ? ( + read + ) : props.messageState === MessageState.Read ? ( + <> + read + read + + ) : ( + read + )} + +
+
+ ); +} diff --git a/packages/messenger-widget/src/components/MessageInput/MessageInput.css b/packages/messenger-widget/src/components/MessageInput/MessageInput.css new file mode 100644 index 000000000..651418a55 --- /dev/null +++ b/packages/messenger-widget/src/components/MessageInput/MessageInput.css @@ -0,0 +1,24 @@ +.chat-action { + bottom: 0; +} + +.chat-action-items { + border: 1px solid var(--text-secondary-color); + padding: 0.2rem; +} + +.chat-svg-icon { + height: 24px; +} + +.smile-icon { + margin-left: 0.3rem; + font-size: x-large; +} + +.text-input-field { + border: none; + outline: none; + padding-left: 0.3rem; + padding-right: 0.9rem; +} diff --git a/packages/messenger-widget/src/components/MessageInput/MessageInput.tsx b/packages/messenger-widget/src/components/MessageInput/MessageInput.tsx new file mode 100644 index 000000000..8e8421bdd --- /dev/null +++ b/packages/messenger-widget/src/components/MessageInput/MessageInput.tsx @@ -0,0 +1,92 @@ +import './MessageInput.css'; +import sendBtnIcon from '../../assets/images/send-btn.svg'; +import fileIcon from '../../assets/images/file.svg'; +import emojiIcon from '../../assets/images/emoji.svg'; +import { useContext, useState } from 'react'; +import { GlobalContext } from '../../utils/context-utils'; +import { handleSubmit } from './bl'; + +export function MessageInput() { + const [message, setMessage] = useState(''); + + const { state, dispatch } = useContext(GlobalContext); + + function setMessageContent(e: React.ChangeEvent) { + setMessage(e.target.value); + } + + return ( + <> + {/* Message emoji, file & input window */} +
+
+
+
+ + file + + + emoji + + | +
+
+ handleSubmit( + message, + state, + dispatch, + setMessage, + event, + ) + } + > + , + ) => setMessageContent(e)} + > +
+ + send, + ) => + handleSubmit( + message, + state, + dispatch, + setMessage, + event, + ) + } + /> + +
+
+
+ + ); +} diff --git a/packages/messenger-widget/src/components/MessageInput/bl.ts b/packages/messenger-widget/src/components/MessageInput/bl.ts new file mode 100644 index 000000000..3a259454c --- /dev/null +++ b/packages/messenger-widget/src/components/MessageInput/bl.ts @@ -0,0 +1,84 @@ +import { createMessage, SendDependencies } from 'dm3-lib-messaging'; +import { log } from 'dm3-lib-shared'; +import { StorageEnvelopContainer } from 'dm3-lib-storage'; +import { submitMessage } from '../../adapters/messages'; +import { Actions, GlobalState, UserDbType } from '../../utils/enum-type-utils'; + +const handleNewUserMessage = async ( + message: string, + setMessage: Function, + state: GlobalState, + dispatch: React.Dispatch, +) => { + const userDb = state.userDb; + + if (!userDb) { + throw Error('userDB not found'); + } + + if (!state.accounts.selectedContact) { + throw Error('no contact selected'); + } + + const haltDelivery = + state.accounts.selectedContact?.account.profile?.publicEncryptionKey && + state.connection.account?.profile?.publicEncryptionKey + ? false + : true; + + const messageData = await createMessage( + state.accounts.selectedContact.account.ensName, + state.connection.account!.ensName, + message, + userDb.keys.signingKeyPair.privateKey, + ); + + const sendDependencies: SendDependencies = { + deliverServiceProfile: + state.accounts.selectedContact.deliveryServiceProfile!, + from: state.connection.account!, + to: state.accounts.selectedContact.account, + keys: userDb.keys, + }; + + try { + await submitMessage( + state.connection, + state.auth.currentSession?.token!, + sendDependencies, + messageData, + haltDelivery, + (envelops: StorageEnvelopContainer[]) => + envelops.forEach((envelop) => + dispatch({ + type: UserDbType.addMessage, + payload: { + container: envelop, + connection: state.connection, + }, + }), + ), + ); + + // empty input field + setMessage(''); + } catch (e) { + log('[handleNewUserMessage] ' + JSON.stringify(e), 'error'); + } +}; + +export const handleSubmit = async ( + message: string, + state: GlobalState, + dispatch: React.Dispatch, + setMessage: Function, + event: + | React.FormEvent + | React.MouseEvent, +) => { + event.preventDefault(); + if (!message.trim().length) { + return; + } + await handleNewUserMessage(message, setMessage, state, dispatch); +}; diff --git a/packages/messenger-widget/src/interfaces/props.ts b/packages/messenger-widget/src/interfaces/props.ts index e10232b99..24d0ed460 100644 --- a/packages/messenger-widget/src/interfaces/props.ts +++ b/packages/messenger-widget/src/interfaces/props.ts @@ -1,3 +1,4 @@ +import { MessageState } from 'dm3-lib-messaging'; import { Actions, GlobalState } from '../utils/enum-type-utils'; import { Config, Dm3Props } from './config'; import { ContactPreview } from './utils'; @@ -20,3 +21,10 @@ export interface ContactMenu { contactDetails: ContactPreview; index: number; } + +export interface MessageProps { + message: string; + time: string; + messageState: MessageState; + ownMessage: boolean; +} diff --git a/packages/messenger-widget/src/styles/common.css b/packages/messenger-widget/src/styles/common.css index cbe4a78ea..b66442c14 100644 --- a/packages/messenger-widget/src/styles/common.css +++ b/packages/messenger-widget/src/styles/common.css @@ -181,6 +181,7 @@ input:focus { ::-webkit-scrollbar { width: 10px; background-color: var(--background-config-box); + border-radius: 10px; } ::-webkit-scrollbar-thumb {