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 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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
{" "}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
addMembers: () => undefined,
|
||||
nickname: undefined,
|
||||
subscriptionsDispatch: () => undefined,
|
||||
});
|
||||
|
||||
export function useMessengerContext() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,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;
|
||||
|
|
|
@ -12,6 +12,7 @@ export class ChatMessage {
|
|||
sender: string;
|
||||
image?: string;
|
||||
responseTo?: string;
|
||||
quote?: ChatMessage;
|
||||
id: string;
|
||||
|
||||
constructor(
|
||||
|
|
Loading…
Reference in New Issue