Fix and refactor activity center (#212)
This commit is contained in:
parent
c810a2943e
commit
51b85b5b49
|
@ -1,8 +1,8 @@
|
||||||
import React, { useMemo, useRef, useState } from "react";
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { useActivities } from "../../contexts/activityProvider";
|
|
||||||
import { useIdentity } from "../../contexts/identityProvider";
|
import { useIdentity } from "../../contexts/identityProvider";
|
||||||
|
import { useActivities } from "../../hooks/useActivities";
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||||
import { TopBtn } from "../Chat/ChatTopbar";
|
import { TopBtn } from "../Chat/ChatTopbar";
|
||||||
import { ActivityIcon } from "../Icons/ActivityIcon";
|
import { ActivityIcon } from "../Icons/ActivityIcon";
|
||||||
|
@ -14,13 +14,17 @@ interface ActivityButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityButton({ className }: ActivityButtonProps) {
|
export function ActivityButton({ className }: ActivityButtonProps) {
|
||||||
const { activities } = useActivities();
|
const { activities, activityDispatch } = useActivities();
|
||||||
const identity = useIdentity();
|
const identity = useIdentity();
|
||||||
const disabled = useMemo(() => !identity, [identity]);
|
const disabled = useMemo(() => !identity, [identity]);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
useClickOutside(ref, () => setShowActivityCenter(false));
|
useClickOutside(ref, () => setShowActivityCenter(false));
|
||||||
|
|
||||||
const [showActivityCenter, setShowActivityCenter] = useState(false);
|
const [showActivityCenter, setShowActivityCenter] = useState(false);
|
||||||
|
const badgeAmount = useMemo(
|
||||||
|
() => activities.filter((activity) => !activity.isRead).length,
|
||||||
|
[activities]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActivityWrapper ref={ref} className={className}>
|
<ActivityWrapper ref={ref} className={className}>
|
||||||
|
@ -29,22 +33,26 @@ export function ActivityButton({ className }: ActivityButtonProps) {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<ActivityIcon />
|
<ActivityIcon />
|
||||||
{activities.length > 0 && (
|
{badgeAmount > 0 && (
|
||||||
<NotificationBagde
|
<NotificationBagde
|
||||||
className={
|
className={
|
||||||
activities.length > 99
|
badgeAmount > 99
|
||||||
? "countless"
|
? "countless"
|
||||||
: activities.length > 9
|
: badgeAmount > 9
|
||||||
? "wide"
|
? "wide"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{activities.length < 100 ? activities.length : "∞"}
|
{badgeAmount < 100 ? badgeAmount : "∞"}
|
||||||
</NotificationBagde>
|
</NotificationBagde>
|
||||||
)}
|
)}
|
||||||
</TopBtn>
|
</TopBtn>
|
||||||
{showActivityCenter && (
|
{showActivityCenter && (
|
||||||
<ActivityCenter setShowActivityCenter={setShowActivityCenter} />
|
<ActivityCenter
|
||||||
|
activities={activities}
|
||||||
|
setShowActivityCenter={setShowActivityCenter}
|
||||||
|
activityDispatch={activityDispatch}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</ActivityWrapper>
|
</ActivityWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { useActivities } from "../../contexts/activityProvider";
|
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
|
import { ActivityAction } from "../../hooks/useActivities";
|
||||||
|
import { Activity } from "../../models/Activity";
|
||||||
import { buttonTransparentStyles } from "../Buttons/buttonStyle";
|
import { buttonTransparentStyles } from "../Buttons/buttonStyle";
|
||||||
import { Tooltip } from "../Form/Tooltip";
|
import { Tooltip } from "../Form/Tooltip";
|
||||||
import { HideIcon } from "../Icons/HideIcon";
|
import { HideIcon } from "../Icons/HideIcon";
|
||||||
|
@ -12,11 +13,16 @@ import { ShowIcon } from "../Icons/ShowIcon";
|
||||||
import { ActivityMessage } from "./ActivityMessage";
|
import { ActivityMessage } from "./ActivityMessage";
|
||||||
|
|
||||||
interface ActivityCenterProps {
|
interface ActivityCenterProps {
|
||||||
|
activities: Activity[];
|
||||||
setShowActivityCenter: (val: boolean) => void;
|
setShowActivityCenter: (val: boolean) => void;
|
||||||
|
activityDispatch: React.Dispatch<ActivityAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
|
export function ActivityCenter({
|
||||||
const { activities } = useActivities();
|
activities,
|
||||||
|
setShowActivityCenter,
|
||||||
|
activityDispatch,
|
||||||
|
}: ActivityCenterProps) {
|
||||||
const { contacts } = useMessengerContext();
|
const { contacts } = useMessengerContext();
|
||||||
|
|
||||||
const shownActivities = useMemo(
|
const shownActivities = useMemo(
|
||||||
|
@ -53,9 +59,7 @@ export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
|
||||||
<Btns>
|
<Btns>
|
||||||
<BtnWrapper>
|
<BtnWrapper>
|
||||||
<ActivityBtn
|
<ActivityBtn
|
||||||
onClick={() => {
|
onClick={() => activityDispatch({ type: "setAllAsRead" })}
|
||||||
shownActivities.map((activity) => (activity.isRead = true));
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ReadIcon />
|
<ReadIcon />
|
||||||
</ActivityBtn>
|
</ActivityBtn>
|
||||||
|
@ -76,6 +80,7 @@ export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
|
||||||
key={activity.id}
|
key={activity.id}
|
||||||
activity={activity}
|
activity={activity}
|
||||||
setShowActivityCenter={setShowActivityCenter}
|
setShowActivityCenter={setShowActivityCenter}
|
||||||
|
activityDispatch={activityDispatch}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Activities>
|
</Activities>
|
||||||
|
|
|
@ -3,7 +3,9 @@ import styled from "styled-components";
|
||||||
|
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
import { useModal } from "../../contexts/modalProvider";
|
import { useModal } from "../../contexts/modalProvider";
|
||||||
|
import { ActivityAction } from "../../hooks/useActivities";
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||||
|
import { useScrollToMessage } from "../../hooks/useScrollToMessage";
|
||||||
import { Activity } from "../../models/Activity";
|
import { Activity } from "../../models/Activity";
|
||||||
import { equalDate } from "../../utils/equalDate";
|
import { equalDate } from "../../utils/equalDate";
|
||||||
import { DownloadButton } from "../Buttons/DownloadButton";
|
import { DownloadButton } from "../Buttons/DownloadButton";
|
||||||
|
@ -41,16 +43,19 @@ const today = new Date();
|
||||||
type ActivityMessageProps = {
|
type ActivityMessageProps = {
|
||||||
activity: Activity;
|
activity: Activity;
|
||||||
setShowActivityCenter: (val: boolean) => void;
|
setShowActivityCenter: (val: boolean) => void;
|
||||||
|
activityDispatch: React.Dispatch<ActivityAction>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ActivityMessage({
|
export function ActivityMessage({
|
||||||
activity,
|
activity,
|
||||||
setShowActivityCenter,
|
setShowActivityCenter,
|
||||||
|
activityDispatch,
|
||||||
}: ActivityMessageProps) {
|
}: ActivityMessageProps) {
|
||||||
const { contacts, channelsDispatch } = useMessengerContext();
|
const { contacts, channelsDispatch } = useMessengerContext();
|
||||||
|
const scroll = useScrollToMessage();
|
||||||
const { setModal } = useModal(ProfileModalName);
|
const { setModal } = useModal(ProfileModalName);
|
||||||
const showChannel = () => {
|
const showChannel = () => {
|
||||||
activity.channel &&
|
"channel" in activity &&
|
||||||
channelsDispatch({ type: "ChangeActive", payload: activity.channel.id }),
|
channelsDispatch({ type: "ChangeActive", payload: activity.channel.id }),
|
||||||
setShowActivityCenter(false);
|
setShowActivityCenter(false);
|
||||||
};
|
};
|
||||||
|
@ -66,10 +71,10 @@ export function ActivityMessage({
|
||||||
|
|
||||||
const [elements, setElements] = useState<
|
const [elements, setElements] = useState<
|
||||||
(string | React.ReactElement | undefined)[]
|
(string | React.ReactElement | undefined)[]
|
||||||
>([activity.message?.content]);
|
>(["message" in activity ? activity.message?.content : undefined]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activity.message) {
|
if ("message" in activity) {
|
||||||
const split = activity.message?.content.split(" ");
|
const split = activity.message?.content.split(" ");
|
||||||
const newSplit = split.flatMap((element, idx) => {
|
const newSplit = split.flatMap((element, idx) => {
|
||||||
if (element.startsWith("@")) {
|
if (element.startsWith("@")) {
|
||||||
|
@ -88,7 +93,7 @@ export function ActivityMessage({
|
||||||
newSplit.pop();
|
newSplit.pop();
|
||||||
setElements(newSplit);
|
setElements(newSplit);
|
||||||
}
|
}
|
||||||
}, [activity.message?.content]);
|
}, [activity]);
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
useClickOutside(ref, () => setShowMenu(false));
|
useClickOutside(ref, () => setShowMenu(false));
|
||||||
|
@ -162,10 +167,16 @@ export function ActivityMessage({
|
||||||
</FlexDiv>
|
</FlexDiv>
|
||||||
)}
|
)}
|
||||||
<ActivityText>
|
<ActivityText>
|
||||||
{activity.message?.content && (
|
{"message" in activity && activity.message?.content && (
|
||||||
<div>{elements.map((el) => el)}</div>
|
<div
|
||||||
|
onClick={() => scroll(activity.message, activity.channel.id)}
|
||||||
|
>
|
||||||
|
{elements.map((el) => el)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{activity.requestType === "income" && activity.request}
|
{activity.type === "request" &&
|
||||||
|
activity.requestType === "income" &&
|
||||||
|
activity.request}
|
||||||
</ActivityText>
|
</ActivityText>
|
||||||
{type === "mention" &&
|
{type === "mention" &&
|
||||||
activity.channel &&
|
activity.channel &&
|
||||||
|
@ -199,8 +210,10 @@ export function ActivityMessage({
|
||||||
<>
|
<>
|
||||||
<ActivityBtn
|
<ActivityBtn
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
activity.isRead = true;
|
activityDispatch({
|
||||||
activity.status = "accepted";
|
type: "setStatus",
|
||||||
|
payload: { id: activity.id, status: "accepted" },
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="accept"
|
className="accept"
|
||||||
>
|
>
|
||||||
|
@ -208,8 +221,10 @@ export function ActivityMessage({
|
||||||
</ActivityBtn>
|
</ActivityBtn>
|
||||||
<ActivityBtn
|
<ActivityBtn
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
activity.isRead = true;
|
activityDispatch({
|
||||||
activity.status = "declined";
|
type: "setStatus",
|
||||||
|
payload: { id: activity.id, status: "declined" },
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="decline"
|
className="decline"
|
||||||
>
|
>
|
||||||
|
@ -240,9 +255,9 @@ export function ActivityMessage({
|
||||||
{(type === "mention" || type === "reply") && (
|
{(type === "mention" || type === "reply") && (
|
||||||
<BtnWrapper>
|
<BtnWrapper>
|
||||||
<ActivityBtn
|
<ActivityBtn
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
activity.isRead = true;
|
activityDispatch({ type: "setAsRead", payload: activity.id })
|
||||||
}}
|
}
|
||||||
className={`${activity.isRead && "read"}`}
|
className={`${activity.isRead && "read"}`}
|
||||||
>
|
>
|
||||||
<ReadMessageIcon isRead={activity.isRead} />
|
<ReadMessageIcon isRead={activity.isRead} />
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { ThemeProvider } from "styled-components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { ConfigType } from "..";
|
import { ConfigType } from "..";
|
||||||
import { ActivityProvider } from "../contexts/activityProvider";
|
|
||||||
import { ChatStateProvider } from "../contexts/chatStateProvider";
|
import { ChatStateProvider } from "../contexts/chatStateProvider";
|
||||||
import { ConfigProvider } from "../contexts/configProvider";
|
import { ConfigProvider } from "../contexts/configProvider";
|
||||||
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
|
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
|
||||||
|
@ -38,7 +37,6 @@ export function DappConnectCommunityChat({
|
||||||
<NarrowProvider myRef={ref}>
|
<NarrowProvider myRef={ref}>
|
||||||
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
|
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<ActivityProvider>
|
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<Wrapper ref={ref}>
|
<Wrapper ref={ref}>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
|
@ -52,7 +50,6 @@ export function DappConnectCommunityChat({
|
||||||
</IdentityProvider>
|
</IdentityProvider>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</ActivityProvider>
|
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</FetchMetadataProvider>
|
</FetchMetadataProvider>
|
||||||
</NarrowProvider>
|
</NarrowProvider>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useCallback } from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
|
import { useScrollToMessage } from "../../hooks/useScrollToMessage";
|
||||||
import { ChatMessage } from "../../models/ChatMessage";
|
import { ChatMessage } from "../../models/ChatMessage";
|
||||||
import { ReplyOn, ReplyTo } from "../Chat/ChatInput";
|
import { ReplyOn, ReplyTo } from "../Chat/ChatInput";
|
||||||
import { QuoteSvg } from "../Icons/QuoteIcon";
|
import { QuoteSvg } from "../Icons/QuoteIcon";
|
||||||
|
@ -23,30 +24,11 @@ type MessageQuoteProps = {
|
||||||
|
|
||||||
export function MessageQuote({ quote }: MessageQuoteProps) {
|
export function MessageQuote({ quote }: MessageQuoteProps) {
|
||||||
const { contacts } = useMessengerContext();
|
const { contacts } = useMessengerContext();
|
||||||
const quoteClick = useCallback(() => {
|
const scroll = useScrollToMessage();
|
||||||
if (quote) {
|
|
||||||
const quoteDiv = document.getElementById(quote.id);
|
|
||||||
if (quoteDiv) {
|
|
||||||
quoteDiv.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
inline: "center",
|
|
||||||
});
|
|
||||||
quoteDiv.style.background = "lightblue";
|
|
||||||
quoteDiv.style.transition = "background-color 1000ms linear";
|
|
||||||
window.setTimeout(() => {
|
|
||||||
quoteDiv.style.background = "";
|
|
||||||
window.setTimeout(() => {
|
|
||||||
quoteDiv.style.transition = "";
|
|
||||||
}, 1000);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [quote]);
|
|
||||||
|
|
||||||
if (quote && quote.sender) {
|
if (quote && quote.sender) {
|
||||||
return (
|
return (
|
||||||
<QuoteWrapper onClick={quoteClick}>
|
<QuoteWrapper onClick={() => scroll(quote)}>
|
||||||
<QuoteSvg width={22} height={calcHeight(quote)} />
|
<QuoteSvg width={22} height={calcHeight(quote)} />
|
||||||
<QuoteSender>
|
<QuoteSender>
|
||||||
{" "}
|
{" "}
|
||||||
|
|
|
@ -64,13 +64,11 @@ export function MessagesList({ setReply, channel }: MessagesListProps) {
|
||||||
<UiMessage
|
<UiMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
channel={channel}
|
|
||||||
idx={idx}
|
idx={idx}
|
||||||
prevMessage={shownMessages[idx - 1]}
|
prevMessage={shownMessages[idx - 1]}
|
||||||
setLink={setLink}
|
setLink={setLink}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
setReply={setReply}
|
setReply={setReply}
|
||||||
quote={shownMessages.find((msg) => msg.id == message?.responseTo)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</MessagesWrapper>
|
</MessagesWrapper>
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { utils } from "@waku/status-communities/dist/cjs";
|
|
||||||
import { BaseEmoji } from "emoji-mart";
|
import { BaseEmoji } from "emoji-mart";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { useActivities } from "../../contexts/activityProvider";
|
|
||||||
import { useIdentity } from "../../contexts/identityProvider";
|
import { useIdentity } from "../../contexts/identityProvider";
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||||
import { Reply } from "../../hooks/useReply";
|
import { Reply } from "../../hooks/useReply";
|
||||||
import { ChannelData } from "../../models/ChannelData";
|
|
||||||
import { ChatMessage } from "../../models/ChatMessage";
|
import { ChatMessage } from "../../models/ChatMessage";
|
||||||
import { equalDate } from "../../utils";
|
import { equalDate } from "../../utils";
|
||||||
import { ChatMessageContent } from "../Chat/ChatMessageContent";
|
import { ChatMessageContent } from "../Chat/ChatMessageContent";
|
||||||
|
@ -38,27 +35,22 @@ import {
|
||||||
type UiMessageProps = {
|
type UiMessageProps = {
|
||||||
idx: number;
|
idx: number;
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
channel: ChannelData;
|
|
||||||
prevMessage: ChatMessage;
|
prevMessage: ChatMessage;
|
||||||
setImage: (img: string) => void;
|
setImage: (img: string) => void;
|
||||||
setLink: (link: string) => void;
|
setLink: (link: string) => void;
|
||||||
setReply: (val: Reply | undefined) => void;
|
setReply: (val: Reply | undefined) => void;
|
||||||
quote?: ChatMessage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UiMessage({
|
export function UiMessage({
|
||||||
message,
|
message,
|
||||||
channel,
|
|
||||||
idx,
|
idx,
|
||||||
prevMessage,
|
prevMessage,
|
||||||
setImage,
|
setImage,
|
||||||
setLink,
|
setLink,
|
||||||
setReply,
|
setReply,
|
||||||
quote,
|
|
||||||
}: UiMessageProps) {
|
}: UiMessageProps) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const { contacts } = useMessengerContext();
|
const { contacts } = useMessengerContext();
|
||||||
const { setActivities } = useActivities();
|
|
||||||
const identity = useIdentity();
|
const identity = useIdentity();
|
||||||
|
|
||||||
const contact = useMemo(
|
const contact = useMemo(
|
||||||
|
@ -70,38 +62,6 @@ export function UiMessage({
|
||||||
const [mentioned, setMentioned] = useState(false);
|
const [mentioned, setMentioned] = useState(false);
|
||||||
const [messageReactions, setMessageReactions] = useState<BaseEmoji[]>([]);
|
const [messageReactions, setMessageReactions] = useState<BaseEmoji[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mentioned)
|
|
||||||
setActivities((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: message.date.getTime().toString() + message.content,
|
|
||||||
type: "mention",
|
|
||||||
date: message.date,
|
|
||||||
user: message.sender,
|
|
||||||
message: message,
|
|
||||||
channel: channel,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
if (
|
|
||||||
quote &&
|
|
||||||
identity &&
|
|
||||||
quote.sender === utils.bufToHex(identity.publicKey)
|
|
||||||
)
|
|
||||||
setActivities((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: message.date.getTime().toString() + message.content,
|
|
||||||
type: "reply",
|
|
||||||
date: message.date,
|
|
||||||
user: message.sender,
|
|
||||||
message: message,
|
|
||||||
channel: channel,
|
|
||||||
quote: quote,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}, [mentioned, message, quote]);
|
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
useClickOutside(ref, () => setShowMenu(false));
|
useClickOutside(ref, () => setShowMenu(false));
|
||||||
|
|
||||||
|
@ -117,7 +77,7 @@ export function UiMessage({
|
||||||
</DateSeparator>
|
</DateSeparator>
|
||||||
)}
|
)}
|
||||||
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
|
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
|
||||||
<MessageQuote quote={quote} />
|
<MessageQuote quote={message.quote} />
|
||||||
<UserMessageWrapper ref={messageRef}>
|
<UserMessageWrapper ref={messageRef}>
|
||||||
<IconBtn
|
<IconBtn
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { bufToHex } from "@waku/status-communities/dist/cjs/utils";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { useActivities } from "../../contexts/activityProvider";
|
|
||||||
import { useIdentity } from "../../contexts/identityProvider";
|
import { useIdentity } from "../../contexts/identityProvider";
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
import { useModal } from "../../contexts/modalProvider";
|
import { useModal } from "../../contexts/modalProvider";
|
||||||
|
@ -49,7 +48,6 @@ export const ProfileModal = () => {
|
||||||
[props]
|
[props]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setActivities } = useActivities();
|
|
||||||
const { setToasts } = useToasts();
|
const { setToasts } = useToasts();
|
||||||
const { setModal } = useModal(ProfileModalName);
|
const { setModal } = useModal(ProfileModalName);
|
||||||
|
|
||||||
|
@ -200,19 +198,6 @@ export const ProfileModal = () => {
|
||||||
<Btn
|
<Btn
|
||||||
disabled={!request}
|
disabled={!request}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActivities((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: id + request,
|
|
||||||
type: "request",
|
|
||||||
isRead: true,
|
|
||||||
date: new Date(),
|
|
||||||
user: id,
|
|
||||||
request: request,
|
|
||||||
requestType: "outcome",
|
|
||||||
status: "sent",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
setToasts((prev) => [
|
setToasts((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React, { createContext, useContext, useState } from "react";
|
|
||||||
|
|
||||||
import { Activity } from "../models/Activity";
|
|
||||||
|
|
||||||
const ActivityContext = createContext<{
|
|
||||||
activities: Activity[];
|
|
||||||
setActivities: React.Dispatch<React.SetStateAction<Activity[]>>;
|
|
||||||
}>({
|
|
||||||
activities: [],
|
|
||||||
setActivities: () => undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useActivities() {
|
|
||||||
return useContext(ActivityContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActivityProvider({ children }: ActivityProviderProps) {
|
|
||||||
const [activities, setActivities] = useState<Activity[]>([]);
|
|
||||||
return (
|
|
||||||
<ActivityContext.Provider
|
|
||||||
value={{ activities, setActivities }}
|
|
||||||
children={children}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -27,6 +27,7 @@ const MessengerContext = createContext<MessengerType>({
|
||||||
changeGroupChatName: () => undefined,
|
changeGroupChatName: () => undefined,
|
||||||
addMembers: () => undefined,
|
addMembers: () => undefined,
|
||||||
nickname: undefined,
|
nickname: undefined,
|
||||||
|
subscriptionsDispatch: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useMessengerContext() {
|
export function useMessengerContext() {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { ThemeProvider } from "styled-components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { ConfigType } from "..";
|
import { ConfigType } from "..";
|
||||||
import { ActivityProvider } from "../contexts/activityProvider";
|
|
||||||
import { ChatStateProvider } from "../contexts/chatStateProvider";
|
import { ChatStateProvider } from "../contexts/chatStateProvider";
|
||||||
import { ConfigProvider } from "../contexts/configProvider";
|
import { ConfigProvider } from "../contexts/configProvider";
|
||||||
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
|
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
|
||||||
|
@ -36,7 +35,6 @@ export function DappConnectGroupChat({
|
||||||
<NarrowProvider myRef={ref}>
|
<NarrowProvider myRef={ref}>
|
||||||
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
|
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<ActivityProvider>
|
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<Wrapper ref={ref}>
|
<Wrapper ref={ref}>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
|
@ -50,7 +48,6 @@ export function DappConnectGroupChat({
|
||||||
</IdentityProvider>
|
</IdentityProvider>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</ActivityProvider>
|
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</FetchMetadataProvider>
|
</FetchMetadataProvider>
|
||||||
</NarrowProvider>
|
</NarrowProvider>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useNotifications } from "./useNotifications";
|
||||||
export function useMessages(
|
export function useMessages(
|
||||||
chatId: string,
|
chatId: string,
|
||||||
identity: Identity | undefined,
|
identity: Identity | undefined,
|
||||||
|
subscriptions: ((msg: ChatMessage, id: string) => void)[],
|
||||||
contacts?: Contacts
|
contacts?: Contacts
|
||||||
) {
|
) {
|
||||||
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
|
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
|
||||||
|
@ -28,6 +29,11 @@ export function useMessages(
|
||||||
(newMessage: ChatMessage | undefined, id: string) => {
|
(newMessage: ChatMessage | undefined, id: string) => {
|
||||||
if (newMessage) {
|
if (newMessage) {
|
||||||
contacts?.addContact(newMessage.sender);
|
contacts?.addContact(newMessage.sender);
|
||||||
|
if (newMessage.responseTo) {
|
||||||
|
newMessage.quote = messages[id].find(
|
||||||
|
(msg) => msg.id === newMessage.responseTo
|
||||||
|
);
|
||||||
|
}
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -39,6 +45,7 @@ export function useMessages(
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
subscriptions.forEach((subscription) => subscription(newMessage, id));
|
||||||
incNotification(id);
|
incNotification(id);
|
||||||
if (
|
if (
|
||||||
identity &&
|
identity &&
|
||||||
|
@ -48,7 +55,7 @@ export function useMessages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[contacts, identity]
|
[contacts, identity, subscriptions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addMessage = useCallback(
|
const addMessage = useCallback(
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
Identity,
|
Identity,
|
||||||
Messenger,
|
Messenger,
|
||||||
} from "@waku/status-communities/dist/cjs";
|
} from "@waku/status-communities/dist/cjs";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||||
|
|
||||||
import { useConfig } from "../../contexts/configProvider";
|
import { useConfig } from "../../contexts/configProvider";
|
||||||
import { ChannelData, ChannelsData } from "../../models/ChannelData";
|
import { ChannelData, ChannelsData } from "../../models/ChannelData";
|
||||||
|
@ -50,6 +50,7 @@ export type MessengerType = {
|
||||||
changeGroupChatName: (name: string, chatId: string) => void;
|
changeGroupChatName: (name: string, chatId: string) => void;
|
||||||
addMembers: (members: string[], chatId: string) => void;
|
addMembers: (members: string[], chatId: string) => void;
|
||||||
nickname: string | undefined;
|
nickname: string | undefined;
|
||||||
|
subscriptionsDispatch: (action: SubscriptionAction) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useCreateMessenger(identity: Identity | undefined) {
|
function useCreateMessenger(identity: Identity | undefined) {
|
||||||
|
@ -105,11 +106,54 @@ function useCreateCommunity(
|
||||||
return { community, communityData };
|
return { community, communityData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subscriptions = {
|
||||||
|
[id: string]: (msg: ChatMessage, id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionAction =
|
||||||
|
| {
|
||||||
|
type: "addSubscription";
|
||||||
|
payload: {
|
||||||
|
name: string;
|
||||||
|
subFunction: (msg: ChatMessage, id: string) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| { type: "removeSubscription"; payload: { name: string } };
|
||||||
|
|
||||||
|
function subscriptionReducer(
|
||||||
|
state: Subscriptions,
|
||||||
|
action: SubscriptionAction
|
||||||
|
): Subscriptions {
|
||||||
|
switch (action.type) {
|
||||||
|
case "addSubscription": {
|
||||||
|
if (state[action.payload.name]) {
|
||||||
|
throw new Error("Subscription already exists");
|
||||||
|
}
|
||||||
|
return { ...state, [action.payload.name]: action.payload.subFunction };
|
||||||
|
}
|
||||||
|
case "removeSubscription": {
|
||||||
|
if (state[action.payload.name]) {
|
||||||
|
const newState = { ...state };
|
||||||
|
delete newState[action.payload.name];
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Wrong subscription action type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useMessenger(
|
export function useMessenger(
|
||||||
communityKey: string | undefined,
|
communityKey: string | undefined,
|
||||||
identity: Identity | undefined,
|
identity: Identity | undefined,
|
||||||
newNickname: string | undefined
|
newNickname: string | undefined
|
||||||
) {
|
) {
|
||||||
|
const [subscriptions, subscriptionsDispatch] = useReducer(
|
||||||
|
subscriptionReducer,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const subList = useMemo(() => Object.values(subscriptions), [subscriptions]);
|
||||||
const [channelsState, channelsDispatch] = useChannelsReducer();
|
const [channelsState, channelsDispatch] = useChannelsReducer();
|
||||||
const messenger = useCreateMessenger(identity);
|
const messenger = useCreateMessenger(identity);
|
||||||
const { contacts, contactsDispatch, contactsClass, nickname } = useContacts(
|
const { contacts, contactsDispatch, contactsClass, nickname } = useContacts(
|
||||||
|
@ -135,7 +179,12 @@ export function useMessenger(
|
||||||
messages,
|
messages,
|
||||||
mentions,
|
mentions,
|
||||||
clearMentions,
|
clearMentions,
|
||||||
} = useMessages(channelsState?.activeChannel?.id, identity, contactsClass);
|
} = useMessages(
|
||||||
|
channelsState?.activeChannel?.id,
|
||||||
|
identity,
|
||||||
|
subList,
|
||||||
|
contactsClass
|
||||||
|
);
|
||||||
|
|
||||||
const { community, communityData } = useCreateCommunity(
|
const { community, communityData } = useCreateCommunity(
|
||||||
messenger,
|
messenger,
|
||||||
|
@ -259,6 +308,7 @@ export function useMessenger(
|
||||||
(communityKey && !channelsState.activeChannel.id)
|
(communityKey && !channelsState.activeChannel.id)
|
||||||
);
|
);
|
||||||
}, [communityData, messenger, channelsState]);
|
}, [communityData, messenger, channelsState]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messenger,
|
messenger,
|
||||||
messages,
|
messages,
|
||||||
|
@ -282,5 +332,6 @@ export function useMessenger(
|
||||||
changeGroupChatName,
|
changeGroupChatName,
|
||||||
addMembers,
|
addMembers,
|
||||||
nickname,
|
nickname,
|
||||||
|
subscriptionsDispatch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { bufToHex } from "@waku/status-communities/dist/cjs/utils";
|
||||||
|
import { useEffect, useMemo, useReducer } from "react";
|
||||||
|
|
||||||
|
import { useIdentity } from "../contexts/identityProvider";
|
||||||
|
import { useMessengerContext } from "../contexts/messengerProvider";
|
||||||
|
import { Activities, Activity, ActivityStatus } from "../models/Activity";
|
||||||
|
import { ChatMessage } from "../models/ChatMessage";
|
||||||
|
|
||||||
|
export type ActivityAction =
|
||||||
|
| { type: "addActivity"; payload: Activity }
|
||||||
|
| { type: "removeActivity"; payload: "string" }
|
||||||
|
| { type: "setAsRead"; payload: string }
|
||||||
|
| { type: "setAllAsRead" }
|
||||||
|
| { type: "setStatus"; payload: { id: string; status: ActivityStatus } };
|
||||||
|
|
||||||
|
function activityReducer(
|
||||||
|
state: Activities,
|
||||||
|
action: ActivityAction
|
||||||
|
): Activities {
|
||||||
|
switch (action.type) {
|
||||||
|
case "setStatus": {
|
||||||
|
const activity = state[action.payload.id];
|
||||||
|
if (activity && "status" in activity) {
|
||||||
|
activity.status = action.payload.status;
|
||||||
|
activity.isRead = true;
|
||||||
|
return { ...state, [activity.id]: activity };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case "setAsRead": {
|
||||||
|
const activity = state[action.payload];
|
||||||
|
if (activity) {
|
||||||
|
activity.isRead = true;
|
||||||
|
return { ...state, [activity.id]: activity };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case "setAllAsRead": {
|
||||||
|
return Object.entries(state).reduce((prev, curr) => {
|
||||||
|
const activity = curr[1];
|
||||||
|
activity.isRead = true;
|
||||||
|
return { ...prev, [curr[0]]: activity };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
case "addActivity": {
|
||||||
|
return { ...state, [action.payload.id]: action.payload };
|
||||||
|
}
|
||||||
|
case "removeActivity": {
|
||||||
|
if (state[action.payload]) {
|
||||||
|
const newState = { ...state };
|
||||||
|
delete newState[action.payload];
|
||||||
|
return newState;
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Wrong activity reducer type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActivities() {
|
||||||
|
const [activitiesObj, dispatch] = useReducer(activityReducer, {});
|
||||||
|
const activities = useMemo(
|
||||||
|
() => Object.values(activitiesObj),
|
||||||
|
[activitiesObj]
|
||||||
|
);
|
||||||
|
const identity = useIdentity();
|
||||||
|
const userPK = useMemo(
|
||||||
|
() => (identity ? bufToHex(identity.publicKey) : undefined),
|
||||||
|
[identity]
|
||||||
|
);
|
||||||
|
const { subscriptionsDispatch, channels } = useMessengerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (identity) {
|
||||||
|
const subscribeFunction = (message: ChatMessage, id: string) => {
|
||||||
|
if (message.quote && identity && message.quote.sender === userPK) {
|
||||||
|
const newActivity: Activity = {
|
||||||
|
id: message.date.getTime().toString() + message.content,
|
||||||
|
type: "reply",
|
||||||
|
date: message.date,
|
||||||
|
user: message.sender,
|
||||||
|
message: message,
|
||||||
|
channel: channels[id],
|
||||||
|
quote: message.quote,
|
||||||
|
};
|
||||||
|
dispatch({ type: "addActivity", payload: newActivity });
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = message.content.split(" ");
|
||||||
|
const userMentioned = split.some(
|
||||||
|
(fragment) => fragment.startsWith("@") && fragment.slice(1) == userPK
|
||||||
|
);
|
||||||
|
if (userMentioned) {
|
||||||
|
const newActivity: Activity = {
|
||||||
|
id: message.date.getTime().toString() + message.content,
|
||||||
|
type: "mention",
|
||||||
|
date: message.date,
|
||||||
|
user: message.sender,
|
||||||
|
message: message,
|
||||||
|
channel: channels[id],
|
||||||
|
};
|
||||||
|
dispatch({ type: "addActivity", payload: newActivity });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
subscriptionsDispatch({
|
||||||
|
type: "addSubscription",
|
||||||
|
payload: { name: "activityCenter", subFunction: subscribeFunction },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () =>
|
||||||
|
subscriptionsDispatch({
|
||||||
|
type: "removeSubscription",
|
||||||
|
payload: { name: "activityCenter" },
|
||||||
|
});
|
||||||
|
}, [subscriptionsDispatch, identity]);
|
||||||
|
|
||||||
|
return { activities, activityDispatch: dispatch };
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useMessengerContext } from "../contexts/messengerProvider";
|
||||||
|
import { ChatMessage } from "../models/ChatMessage";
|
||||||
|
|
||||||
|
export function useScrollToMessage() {
|
||||||
|
const scrollToDivId = useCallback((id: string) => {
|
||||||
|
const quoteDiv = document.getElementById(id);
|
||||||
|
if (quoteDiv) {
|
||||||
|
quoteDiv.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
inline: "center",
|
||||||
|
});
|
||||||
|
quoteDiv.style.background = "lightblue";
|
||||||
|
quoteDiv.style.transition = "background-color 1000ms linear";
|
||||||
|
window.setTimeout(() => {
|
||||||
|
quoteDiv.style.background = "";
|
||||||
|
window.setTimeout(() => {
|
||||||
|
quoteDiv.style.transition = "";
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { activeChannel, channelsDispatch } = useMessengerContext();
|
||||||
|
const [scrollToMessage, setScrollToMessage] = useState("");
|
||||||
|
const [messageChannel, setMessageChannel] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollToMessage && messageChannel) {
|
||||||
|
if (activeChannel?.id === messageChannel) {
|
||||||
|
scrollToDivId(scrollToMessage);
|
||||||
|
setScrollToMessage("");
|
||||||
|
setMessageChannel("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChannel, scrollToMessage, messageChannel]);
|
||||||
|
|
||||||
|
const scroll = useCallback((msg: ChatMessage, channelId?: string) => {
|
||||||
|
if (!channelId) {
|
||||||
|
scrollToDivId(msg.id);
|
||||||
|
} else {
|
||||||
|
setMessageChannel(channelId);
|
||||||
|
setScrollToMessage(msg.id);
|
||||||
|
channelsDispatch({ type: "ChangeActive", payload: channelId });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return scroll;
|
||||||
|
}
|
|
@ -2,18 +2,45 @@ import { ChannelData } from "./ChannelData";
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "./ChatMessage";
|
||||||
import { CommunityData } from "./CommunityData";
|
import { CommunityData } from "./CommunityData";
|
||||||
|
|
||||||
export type Activity = {
|
export type ActivityStatus = "sent" | "accepted" | "declined" | "blocked";
|
||||||
|
|
||||||
|
export type Activity =
|
||||||
|
| {
|
||||||
id: string;
|
id: string;
|
||||||
type: "mention" | "request" | "reply" | "invitation";
|
type: "mention";
|
||||||
|
date: Date;
|
||||||
|
user: string;
|
||||||
|
message: ChatMessage;
|
||||||
|
channel: ChannelData;
|
||||||
|
isRead?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
type: "reply";
|
||||||
|
date: Date;
|
||||||
|
user: string;
|
||||||
|
message: ChatMessage;
|
||||||
|
channel: ChannelData;
|
||||||
|
quote: ChatMessage;
|
||||||
|
isRead?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
type: "request";
|
||||||
|
date: Date;
|
||||||
|
user: string;
|
||||||
|
isRead?: boolean;
|
||||||
|
request: string;
|
||||||
|
requestType: "outcome" | "income";
|
||||||
|
status: ActivityStatus;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
type: "invitation";
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
date: Date;
|
date: Date;
|
||||||
user: string;
|
user: string;
|
||||||
message?: ChatMessage;
|
status: ActivityStatus;
|
||||||
channel?: ChannelData;
|
|
||||||
request?: string;
|
|
||||||
requestType?: "outcome" | "income";
|
|
||||||
status?: "sent" | "accepted" | "declined" | "blocked";
|
|
||||||
quote?: ChatMessage;
|
|
||||||
invitation?: CommunityData;
|
invitation?: CommunityData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ export class ChatMessage {
|
||||||
sender: string;
|
sender: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
responseTo?: string;
|
responseTo?: string;
|
||||||
|
quote?: ChatMessage;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
Loading…
Reference in New Issue