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 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>
); );

View File

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

View File

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

View File

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

View File

@ -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>
{" "} {" "}

View File

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

View File

@ -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={() => {

View File

@ -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,
{ {

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, changeGroupChatName: () => undefined,
addMembers: () => undefined, addMembers: () => undefined,
nickname: undefined, nickname: undefined,
subscriptionsDispatch: () => undefined,
}); });
export function useMessengerContext() { export function useMessengerContext() {

View File

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

View File

@ -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(

View File

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

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

View File

@ -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(