Fix and refactor activity center (#212)

This commit is contained in:
Szymon Szlachtowicz 2022-02-01 13:13:30 +01:00 committed by GitHub
parent c810a2943e
commit 51b85b5b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 365 additions and 189 deletions

View File

@ -1,8 +1,8 @@
import React, { useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useActivities } from "../../hooks/useActivities";
import { useClickOutside } from "../../hooks/useClickOutside";
import { TopBtn } from "../Chat/ChatTopbar";
import { ActivityIcon } from "../Icons/ActivityIcon";
@ -14,13 +14,17 @@ interface ActivityButtonProps {
}
export function ActivityButton({ className }: ActivityButtonProps) {
const { activities } = useActivities();
const { activities, activityDispatch } = useActivities();
const identity = useIdentity();
const disabled = useMemo(() => !identity, [identity]);
const ref = useRef(null);
useClickOutside(ref, () => setShowActivityCenter(false));
const [showActivityCenter, setShowActivityCenter] = useState(false);
const badgeAmount = useMemo(
() => activities.filter((activity) => !activity.isRead).length,
[activities]
);
return (
<ActivityWrapper ref={ref} className={className}>
@ -29,22 +33,26 @@ export function ActivityButton({ className }: ActivityButtonProps) {
disabled={disabled}
>
<ActivityIcon />
{activities.length > 0 && (
{badgeAmount > 0 && (
<NotificationBagde
className={
activities.length > 99
badgeAmount > 99
? "countless"
: activities.length > 9
: badgeAmount > 9
? "wide"
: undefined
}
>
{activities.length < 100 ? activities.length : "∞"}
{badgeAmount < 100 ? badgeAmount : "∞"}
</NotificationBagde>
)}
</TopBtn>
{showActivityCenter && (
<ActivityCenter setShowActivityCenter={setShowActivityCenter} />
<ActivityCenter
activities={activities}
setShowActivityCenter={setShowActivityCenter}
activityDispatch={activityDispatch}
/>
)}
</ActivityWrapper>
);

View File

@ -1,8 +1,9 @@
import React, { useMemo, useState } from "react";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { ActivityAction } from "../../hooks/useActivities";
import { Activity } from "../../models/Activity";
import { buttonTransparentStyles } from "../Buttons/buttonStyle";
import { Tooltip } from "../Form/Tooltip";
import { HideIcon } from "../Icons/HideIcon";
@ -12,11 +13,16 @@ import { ShowIcon } from "../Icons/ShowIcon";
import { ActivityMessage } from "./ActivityMessage";
interface ActivityCenterProps {
activities: Activity[];
setShowActivityCenter: (val: boolean) => void;
activityDispatch: React.Dispatch<ActivityAction>;
}
export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
const { activities } = useActivities();
export function ActivityCenter({
activities,
setShowActivityCenter,
activityDispatch,
}: ActivityCenterProps) {
const { contacts } = useMessengerContext();
const shownActivities = useMemo(
@ -53,9 +59,7 @@ export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
<Btns>
<BtnWrapper>
<ActivityBtn
onClick={() => {
shownActivities.map((activity) => (activity.isRead = true));
}}
onClick={() => activityDispatch({ type: "setAllAsRead" })}
>
<ReadIcon />
</ActivityBtn>
@ -76,6 +80,7 @@ export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
key={activity.id}
activity={activity}
setShowActivityCenter={setShowActivityCenter}
activityDispatch={activityDispatch}
/>
))}
</Activities>

View File

@ -3,7 +3,9 @@ import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { ActivityAction } from "../../hooks/useActivities";
import { useClickOutside } from "../../hooks/useClickOutside";
import { useScrollToMessage } from "../../hooks/useScrollToMessage";
import { Activity } from "../../models/Activity";
import { equalDate } from "../../utils/equalDate";
import { DownloadButton } from "../Buttons/DownloadButton";
@ -41,16 +43,19 @@ const today = new Date();
type ActivityMessageProps = {
activity: Activity;
setShowActivityCenter: (val: boolean) => void;
activityDispatch: React.Dispatch<ActivityAction>;
};
export function ActivityMessage({
activity,
setShowActivityCenter,
activityDispatch,
}: ActivityMessageProps) {
const { contacts, channelsDispatch } = useMessengerContext();
const scroll = useScrollToMessage();
const { setModal } = useModal(ProfileModalName);
const showChannel = () => {
activity.channel &&
"channel" in activity &&
channelsDispatch({ type: "ChangeActive", payload: activity.channel.id }),
setShowActivityCenter(false);
};
@ -66,10 +71,10 @@ export function ActivityMessage({
const [elements, setElements] = useState<
(string | React.ReactElement | undefined)[]
>([activity.message?.content]);
>(["message" in activity ? activity.message?.content : undefined]);
useEffect(() => {
if (activity.message) {
if ("message" in activity) {
const split = activity.message?.content.split(" ");
const newSplit = split.flatMap((element, idx) => {
if (element.startsWith("@")) {
@ -88,7 +93,7 @@ export function ActivityMessage({
newSplit.pop();
setElements(newSplit);
}
}, [activity.message?.content]);
}, [activity]);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
@ -162,10 +167,16 @@ export function ActivityMessage({
</FlexDiv>
)}
<ActivityText>
{activity.message?.content && (
<div>{elements.map((el) => el)}</div>
{"message" in activity && activity.message?.content && (
<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>
{type === "mention" &&
activity.channel &&
@ -199,8 +210,10 @@ export function ActivityMessage({
<>
<ActivityBtn
onClick={() => {
activity.isRead = true;
activity.status = "accepted";
activityDispatch({
type: "setStatus",
payload: { id: activity.id, status: "accepted" },
});
}}
className="accept"
>
@ -208,8 +221,10 @@ export function ActivityMessage({
</ActivityBtn>
<ActivityBtn
onClick={() => {
activity.isRead = true;
activity.status = "declined";
activityDispatch({
type: "setStatus",
payload: { id: activity.id, status: "declined" },
});
}}
className="decline"
>
@ -240,9 +255,9 @@ export function ActivityMessage({
{(type === "mention" || type === "reply") && (
<BtnWrapper>
<ActivityBtn
onClick={() => {
activity.isRead = true;
}}
onClick={() =>
activityDispatch({ type: "setAsRead", payload: activity.id })
}
className={`${activity.isRead && "read"}`}
>
<ReadMessageIcon isRead={activity.isRead} />

View File

@ -3,7 +3,6 @@ import { ThemeProvider } from "styled-components";
import styled from "styled-components";
import { ConfigType } from "..";
import { ActivityProvider } from "../contexts/activityProvider";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { ConfigProvider } from "../contexts/configProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
@ -38,7 +37,6 @@ export function DappConnectCommunityChat({
<NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<ModalProvider>
<ActivityProvider>
<ToastProvider>
<Wrapper ref={ref}>
<GlobalStyle />
@ -52,7 +50,6 @@ export function DappConnectCommunityChat({
</IdentityProvider>
</Wrapper>
</ToastProvider>
</ActivityProvider>
</ModalProvider>
</FetchMetadataProvider>
</NarrowProvider>

View File

@ -1,7 +1,8 @@
import React, { useCallback } from "react";
import React from "react";
import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useScrollToMessage } from "../../hooks/useScrollToMessage";
import { ChatMessage } from "../../models/ChatMessage";
import { ReplyOn, ReplyTo } from "../Chat/ChatInput";
import { QuoteSvg } from "../Icons/QuoteIcon";
@ -23,30 +24,11 @@ type MessageQuoteProps = {
export function MessageQuote({ quote }: MessageQuoteProps) {
const { contacts } = useMessengerContext();
const quoteClick = useCallback(() => {
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]);
const scroll = useScrollToMessage();
if (quote && quote.sender) {
return (
<QuoteWrapper onClick={quoteClick}>
<QuoteWrapper onClick={() => scroll(quote)}>
<QuoteSvg width={22} height={calcHeight(quote)} />
<QuoteSender>
{" "}

View File

@ -64,13 +64,11 @@ export function MessagesList({ setReply, channel }: MessagesListProps) {
<UiMessage
key={message.id}
message={message}
channel={channel}
idx={idx}
prevMessage={shownMessages[idx - 1]}
setLink={setLink}
setImage={setImage}
setReply={setReply}
quote={shownMessages.find((msg) => msg.id == message?.responseTo)}
/>
))}
</MessagesWrapper>

View File

@ -1,14 +1,11 @@
import { utils } from "@waku/status-communities/dist/cjs";
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 { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils";
import { ChatMessageContent } from "../Chat/ChatMessageContent";
@ -38,27 +35,22 @@ import {
type UiMessageProps = {
idx: number;
message: ChatMessage;
channel: ChannelData;
prevMessage: ChatMessage;
setImage: (img: string) => void;
setLink: (link: string) => void;
setReply: (val: Reply | undefined) => void;
quote?: ChatMessage;
};
export function UiMessage({
message,
channel,
idx,
prevMessage,
setImage,
setLink,
setReply,
quote,
}: UiMessageProps) {
const today = new Date();
const { contacts } = useMessengerContext();
const { setActivities } = useActivities();
const identity = useIdentity();
const contact = useMemo(
@ -70,38 +62,6 @@ export function UiMessage({
const [mentioned, setMentioned] = useState(false);
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);
useClickOutside(ref, () => setShowMenu(false));
@ -117,7 +77,7 @@ export function UiMessage({
</DateSeparator>
)}
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
<MessageQuote quote={quote} />
<MessageQuote quote={message.quote} />
<UserMessageWrapper ref={messageRef}>
<IconBtn
onClick={() => {

View File

@ -2,7 +2,6 @@ import { bufToHex } from "@waku/status-communities/dist/cjs/utils";
import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
@ -49,7 +48,6 @@ export const ProfileModal = () => {
[props]
);
const { setActivities } = useActivities();
const { setToasts } = useToasts();
const { setModal } = useModal(ProfileModalName);
@ -200,19 +198,6 @@ export const ProfileModal = () => {
<Btn
disabled={!request}
onClick={() => {
setActivities((prev) => [
...prev,
{
id: id + request,
type: "request",
isRead: true,
date: new Date(),
user: id,
request: request,
requestType: "outcome",
status: "sent",
},
]),
setToasts((prev) => [
...prev,
{

View File

@ -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}
/>
);
}

View File

@ -27,6 +27,7 @@ const MessengerContext = createContext<MessengerType>({
changeGroupChatName: () => undefined,
addMembers: () => undefined,
nickname: undefined,
subscriptionsDispatch: () => undefined,
});
export function useMessengerContext() {

View File

@ -3,7 +3,6 @@ import { ThemeProvider } from "styled-components";
import styled from "styled-components";
import { ConfigType } from "..";
import { ActivityProvider } from "../contexts/activityProvider";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { ConfigProvider } from "../contexts/configProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
@ -36,7 +35,6 @@ export function DappConnectGroupChat({
<NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<ModalProvider>
<ActivityProvider>
<ToastProvider>
<Wrapper ref={ref}>
<GlobalStyle />
@ -50,7 +48,6 @@ export function DappConnectGroupChat({
</IdentityProvider>
</Wrapper>
</ToastProvider>
</ActivityProvider>
</ModalProvider>
</FetchMetadataProvider>
</NarrowProvider>

View File

@ -14,6 +14,7 @@ import { useNotifications } from "./useNotifications";
export function useMessages(
chatId: string,
identity: Identity | undefined,
subscriptions: ((msg: ChatMessage, id: string) => void)[],
contacts?: Contacts
) {
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
@ -28,6 +29,11 @@ export function useMessages(
(newMessage: ChatMessage | undefined, id: string) => {
if (newMessage) {
contacts?.addContact(newMessage.sender);
if (newMessage.responseTo) {
newMessage.quote = messages[id].find(
(msg) => msg.id === newMessage.responseTo
);
}
setMessages((prev) => {
return {
...prev,
@ -39,6 +45,7 @@ export function useMessages(
),
};
});
subscriptions.forEach((subscription) => subscription(newMessage, id));
incNotification(id);
if (
identity &&
@ -48,7 +55,7 @@ export function useMessages(
}
}
},
[contacts, identity]
[contacts, identity, subscriptions]
);
const addMessage = useCallback(

View File

@ -6,7 +6,7 @@ import {
Identity,
Messenger,
} 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 { ChannelData, ChannelsData } from "../../models/ChannelData";
@ -50,6 +50,7 @@ export type MessengerType = {
changeGroupChatName: (name: string, chatId: string) => void;
addMembers: (members: string[], chatId: string) => void;
nickname: string | undefined;
subscriptionsDispatch: (action: SubscriptionAction) => void;
};
function useCreateMessenger(identity: Identity | undefined) {
@ -105,11 +106,54 @@ function useCreateCommunity(
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(
communityKey: string | undefined,
identity: Identity | undefined,
newNickname: string | undefined
) {
const [subscriptions, subscriptionsDispatch] = useReducer(
subscriptionReducer,
{}
);
const subList = useMemo(() => Object.values(subscriptions), [subscriptions]);
const [channelsState, channelsDispatch] = useChannelsReducer();
const messenger = useCreateMessenger(identity);
const { contacts, contactsDispatch, contactsClass, nickname } = useContacts(
@ -135,7 +179,12 @@ export function useMessenger(
messages,
mentions,
clearMentions,
} = useMessages(channelsState?.activeChannel?.id, identity, contactsClass);
} = useMessages(
channelsState?.activeChannel?.id,
identity,
subList,
contactsClass
);
const { community, communityData } = useCreateCommunity(
messenger,
@ -259,6 +308,7 @@ export function useMessenger(
(communityKey && !channelsState.activeChannel.id)
);
}, [communityData, messenger, channelsState]);
return {
messenger,
messages,
@ -282,5 +332,6 @@ export function useMessenger(
changeGroupChatName,
addMembers,
nickname,
subscriptionsDispatch,
};
}

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -2,20 +2,47 @@ import { ChannelData } from "./ChannelData";
import { ChatMessage } from "./ChatMessage";
import { CommunityData } from "./CommunityData";
export type Activity = {
export type ActivityStatus = "sent" | "accepted" | "declined" | "blocked";
export type Activity =
| {
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;
date: Date;
user: string;
message?: ChatMessage;
channel?: ChannelData;
request?: string;
requestType?: "outcome" | "income";
status?: "sent" | "accepted" | "declined" | "blocked";
quote?: ChatMessage;
status: ActivityStatus;
invitation?: CommunityData;
};
};
export type Activities = {
[id: string]: Activity;

View File

@ -12,6 +12,7 @@ export class ChatMessage {
sender: string;
image?: string;
responseTo?: string;
quote?: ChatMessage;
id: string;
constructor(