Refactor hooks and add reducers (#208)

This commit is contained in:
Szymon Szlachtowicz 2022-01-30 11:32:07 +01:00 committed by GitHub
parent f2aa41309a
commit 21e49cb7bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 258 additions and 223 deletions

View File

@ -3,8 +3,8 @@ import React, { useMemo } from "react";
import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
import { AddContactIcon } from "../Icons/AddContactIcon";
import { BlockSvg } from "../Icons/BlockIcon";
import { ChatSvg } from "../Icons/ChatIcon";
@ -26,6 +26,8 @@ type ContactMenuProps = {
export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
const identity = useIdentity();
const { contacts, contactsDispatch } = useMessengerContext();
const contact = useMemo(() => contacts[id], [id, contacts]);
const isUser = useMemo(() => {
if (identity) {
return id === bufToHex(identity.publicKey);
@ -35,7 +37,6 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
}, [id, identity]);
const { setModal } = useModal(ProfileModalName);
const { contact, setBlocked, setIsUntrustworthy } = useManageContact(id);
if (!contact) return null;
return (
@ -88,7 +89,11 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
</MenuItem>
</MenuSection>
<MenuSection>
<MenuItem onClick={() => setIsUntrustworthy(!contact.isUntrustworthy)}>
<MenuItem
onClick={() =>
contactsDispatch({ type: "toggleTrustworthy", payload: { id } })
}
>
<WarningSvg
width={16}
height={16}
@ -104,7 +109,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
{!contact.isFriend && !isUser && (
<MenuItem
onClick={() => {
setBlocked(!contact.blocked);
contactsDispatch({ type: "toggleBlocked", payload: { id } });
setShowMenu(false);
}}
>

View File

@ -4,9 +4,9 @@ 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";
import { useToasts } from "../../contexts/toastProvider";
import { useManageContact } from "../../hooks/useManageContact";
import { copy } from "../../utils";
import { buttonStyles } from "../Buttons/buttonStyle";
import {
@ -75,13 +75,8 @@ export const ProfileModal = () => {
setRequestCreation(requestState ?? false);
}, [requestState]);
const {
contact,
setBlocked,
setCustomName,
setIsUntrustworthy,
setIsUserFriend,
} = useManageContact(id);
const { contacts, contactsDispatch } = useMessengerContext();
const contact = useMemo(() => contacts[id], [id, contacts]);
const [customNameInput, setCustomNameInput] = useState("");
if (!contact) return null;
@ -129,7 +124,10 @@ export const ProfileModal = () => {
{customNameInput && (
<ClearBtn
onClick={() => {
setCustomName(undefined);
contactsDispatch({
type: "setCustomName",
payload: { id, customName: undefined },
});
setCustomNameInput("");
}}
>
@ -184,7 +182,10 @@ export const ProfileModal = () => {
<Btn
disabled={!customNameInput}
onClick={() => {
setCustomName(customNameInput);
contactsDispatch({
type: "setCustomName",
payload: { id, customName: customNameInput },
});
setRenaming(false);
}}
>
@ -234,7 +235,7 @@ export const ProfileModal = () => {
<ProfileBtn
className={contact.blocked ? "" : "red"}
onClick={() => {
setBlocked(!contact.blocked);
contactsDispatch({ type: "toggleBlocked", payload: { id } });
}}
>
{contact.blocked ? "Unblock" : "Block"}
@ -243,14 +244,21 @@ export const ProfileModal = () => {
{contact.isFriend && (
<ProfileBtn
className="red"
onClick={() => setIsUserFriend(false)}
onClick={() =>
contactsDispatch({
type: "setIsFriend",
payload: { id, isFriend: false },
})
}
>
Remove Contact
</ProfileBtn>
)}
<ProfileBtn
className={contact.isUntrustworthy ? "" : "red"}
onClick={() => setIsUntrustworthy(!contact.isUntrustworthy)}
onClick={() =>
contactsDispatch({ type: "toggleTrustworthy", payload: { id } })
}
>
{contact.isUntrustworthy
? "Remove Untrustworthy Mark"

View File

@ -17,7 +17,7 @@ const MessengerContext = createContext<MessengerType>({
loadingMessenger: true,
communityData: undefined,
contacts: {},
setContacts: () => undefined,
contactsDispatch: () => undefined,
activeChannel: undefined,
channels: {},
channelsDispatch: () => undefined,

View File

@ -0,0 +1,79 @@
import { useReducer } from "react";
import { ChannelData, ChannelsData } from "../../models/ChannelData";
export type ChannelsState = {
channels: ChannelsData;
activeChannel: ChannelData;
};
export type ChannelAction =
| { type: "AddChannel"; payload: ChannelData }
| { type: "UpdateActive"; payload: ChannelData }
| { type: "ChangeActive"; payload: string }
| { type: "ToggleMuted"; payload: string }
| { type: "RemoveChannel"; payload: string };
function channelReducer(
state: ChannelsState,
action: ChannelAction
): ChannelsState {
switch (action.type) {
case "AddChannel": {
const channels = {
...state.channels,
[action.payload.id]: action.payload,
};
return { channels, activeChannel: action.payload };
}
case "UpdateActive": {
const activeChannel = state.activeChannel;
if (activeChannel) {
return {
channels: { ...state.channels, [activeChannel.id]: action.payload },
activeChannel: action.payload,
};
}
return state;
}
case "ChangeActive": {
const newActive = state.channels[action.payload];
if (newActive) {
return { ...state, activeChannel: newActive };
}
return state;
}
case "ToggleMuted": {
const channel = state.channels[action.payload];
if (channel) {
const updatedChannel: ChannelData = {
...channel,
isMuted: !channel.isMuted,
};
return {
channels: { ...state.channels, [channel.id]: updatedChannel },
activeChannel: updatedChannel,
};
}
return state;
}
case "RemoveChannel": {
const channelsCopy = { ...state.channels };
delete channelsCopy[action.payload];
let newActive = { id: "", name: "", type: "channel" } as ChannelData;
if (Object.values(channelsCopy).length > 0) {
newActive = Object.values(channelsCopy)[0];
}
return { channels: channelsCopy, activeChannel: newActive };
}
default:
throw new Error();
}
}
export function useChannelsReducer() {
return useReducer(channelReducer, {
channels: {},
activeChannel: { id: "", name: "", type: "channel" },
} as ChannelsState);
}

View File

@ -4,36 +4,116 @@ import {
Messenger,
} from "@waku/status-communities/dist/cjs";
import { bufToHex } from "@waku/status-communities/dist/cjs/utils";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useReducer, useState } from "react";
import { Contacts } from "../../models/Contact";
export type ContactsAction =
| { type: "updateOnline"; payload: { id: string; clock: number } }
| { type: "setTrueName"; payload: { id: string; trueName: string } }
| {
type: "setCustomName";
payload: { id: string; customName: string | undefined };
}
| {
type: "setIsUntrustworthy";
payload: { id: string; isUntrustworthy: boolean };
}
| { type: "setIsFriend"; payload: { id: string; isFriend: boolean } }
| { type: "setBlocked"; payload: { id: string; blocked: boolean } }
| { type: "toggleBlocked"; payload: { id: string } }
| { type: "toggleTrustworthy"; payload: { id: string } };
function contactsReducer(state: Contacts, action: ContactsAction): Contacts {
const id = action.payload.id;
const prev = state[id];
switch (action.type) {
case "updateOnline": {
const now = Date.now();
const clock = action.payload.clock;
if (prev) {
return { ...state, [id]: { ...prev, online: clock > now - 301000 } };
}
return { ...state, [id]: { id, trueName: id.slice(0, 10) } };
}
case "setTrueName": {
const trueName = action.payload.trueName;
if (prev) {
return { ...state, [id]: { ...prev, trueName } };
}
return { ...state, [id]: { id, trueName } };
}
case "setCustomName": {
const customName = action.payload.customName;
if (prev) {
return { ...state, [id]: { ...prev, customName } };
}
return state;
}
case "setIsUntrustworthy": {
const isUntrustworthy = action.payload.isUntrustworthy;
if (prev) {
return { ...state, [id]: { ...prev, isUntrustworthy } };
}
return state;
}
case "setIsFriend": {
const isFriend = action.payload.isFriend;
if (prev) {
return { ...state, [id]: { ...prev, isFriend } };
}
return state;
}
case "setBlocked": {
const blocked = action.payload.blocked;
if (prev) {
return { ...state, [id]: { ...prev, blocked } };
}
return state;
}
case "toggleBlocked": {
if (prev) {
return { ...state, [id]: { ...prev, blocked: !prev.blocked } };
}
return state;
}
case "toggleTrustworthy": {
if (prev) {
return {
...state,
[id]: { ...prev, isUntrustworthy: !prev.isUntrustworthy },
};
}
return state;
}
default:
throw new Error();
}
}
export function useContacts(
messenger: Messenger | undefined,
identity: Identity | undefined,
newNickname: string | undefined
) {
const [nickname, setNickname] = useState<string | undefined>(undefined);
const [internalContacts, setInternalContacts] = useState<{
[id: string]: { clock: number; nickname?: string };
}>({});
const [contacts, contactsDispatch] = useReducer(contactsReducer, {});
const contactsClass = useMemo(() => {
if (messenger) {
const newContacts = new ContactsClass(
identity,
messenger.waku,
(id, clock) => {
setInternalContacts((prev) => {
return { ...prev, [id]: { ...prev[id], clock } };
});
},
(id, clock) =>
contactsDispatch({ type: "updateOnline", payload: { id, clock } }),
(id, nickname) => {
setInternalContacts((prev) => {
if (identity?.publicKey && id === bufToHex(identity.publicKey)) {
setNickname(nickname);
}
return { ...prev, [id]: { ...prev[id], nickname } };
contactsDispatch({
type: "setTrueName",
payload: { id, trueName: nickname },
});
},
newNickname
@ -42,27 +122,5 @@ export function useContacts(
}
}, [messenger, identity]);
const [contacts, setContacts] = useState<Contacts>({});
useEffect(() => {
const now = Date.now();
setContacts((prev) => {
const newContacts: Contacts = {};
Object.entries(internalContacts).forEach(([id, { clock, nickname }]) => {
newContacts[id] = {
id,
online: clock > now - 301000,
trueName: nickname ?? id.slice(0, 10),
isUntrustworthy: false,
blocked: false,
};
if (prev[id]) {
newContacts[id] = { ...prev[id], ...newContacts[id] };
}
});
return newContacts;
});
}, [internalContacts]);
return { contacts, setContacts, contactsClass, nickname };
return { contacts, contactsDispatch, contactsClass, nickname };
}

View File

@ -12,7 +12,7 @@ import { ChatMessage } from "../../models/ChatMessage";
import { Contact } from "../../models/Contact";
import { uintToImgUrl } from "../../utils";
import { ChannelAction } from "./useMessenger";
import { ChannelAction } from "./useChannelsReducer";
const contactFromId = (member: string): Contact => {
return {
@ -43,13 +43,14 @@ export function useGroupChats(
name: chat.name ?? chat.chatId.slice(0, 10),
type: "group",
description: `${chat.members.length} members`,
members: members,
members,
}
: {
id: chat.chatId,
name: chat.members[0].id,
type: "dm",
description: `Chatkey: ${chat.members[0].id}`,
members,
};
dispatch({ type: "AddChannel", payload: channel });
};

View File

@ -6,7 +6,7 @@ import {
Identity,
Messenger,
} from "@waku/status-communities/dist/cjs";
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useConfig } from "../../contexts/configProvider";
import { ChannelData, ChannelsData } from "../../models/ChannelData";
@ -17,7 +17,8 @@ import { createCommunity } from "../../utils/createCommunity";
import { createMessenger } from "../../utils/createMessenger";
import { uintToImgUrl } from "../../utils/uintToImgUrl";
import { useContacts } from "./useContacts";
import { ChannelAction, useChannelsReducer } from "./useChannelsReducer";
import { ContactsAction, useContacts } from "./useContacts";
import { useGroupChats } from "./useGroupChats";
import { useLoadPrevDay } from "./useLoadPrevDay";
import { useMessages } from "./useMessages";
@ -39,7 +40,7 @@ export type MessengerType = {
loadingMessenger: boolean;
communityData: CommunityData | undefined;
contacts: Contacts;
setContacts: React.Dispatch<React.SetStateAction<Contacts>>;
contactsDispatch: (action: ContactsAction) => void;
channels: ChannelsData;
channelsDispatch: (action: ChannelAction) => void;
removeChannel: (channelId: string) => void;
@ -78,9 +79,12 @@ function useCreateCommunity(
const communityData = useMemo(() => {
if (community?.description) {
Object.keys(community.description.proto.members).forEach((contact) =>
contactsClass?.addContact(contact)
);
const membersList = Object.keys(community.description.proto.members);
if (contactsClass) {
membersList.forEach(contactsClass.addContact, contactsClass);
}
return {
id: community.publicKeyStr,
name: community.description.identity?.displayName ?? "",
@ -88,8 +92,8 @@ function useCreateCommunity(
community.description?.identity?.images?.thumbnail?.payload ??
new Uint8Array()
),
members: 0,
membersList: Object.keys(community.description.proto.members),
members: membersList.length,
membersList,
description: community.description.identity?.description ?? "",
};
} else {
@ -100,88 +104,14 @@ function useCreateCommunity(
return { community, communityData };
}
export type ChannelsState = {
channels: ChannelsData;
activeChannel: ChannelData;
};
export type ChannelAction =
| { type: "AddChannel"; payload: ChannelData }
| { type: "UpdateActive"; payload: ChannelData }
| { type: "ChangeActive"; payload: string }
| { type: "ToggleMuted"; payload: string }
| { type: "RemoveChannel"; payload: string };
function channelReducer(
state: ChannelsState,
action: ChannelAction
): ChannelsState {
switch (action.type) {
case "AddChannel": {
const channels = {
...state.channels,
[action.payload.id]: action.payload,
};
return { channels, activeChannel: action.payload };
}
case "UpdateActive": {
const activeChannel = state.activeChannel;
if (activeChannel) {
return {
channels: { ...state.channels, [activeChannel.id]: action.payload },
activeChannel: action.payload,
};
}
return state;
}
case "ChangeActive": {
const newActive = state.channels[action.payload];
if (newActive) {
return { ...state, activeChannel: newActive };
}
return state;
}
case "ToggleMuted": {
const channel = state.channels[action.payload];
if (channel) {
const updatedChannel: ChannelData = {
...channel,
isMuted: !channel.isMuted,
};
return {
channels: { ...state.channels, [channel.id]: updatedChannel },
activeChannel: updatedChannel,
};
}
return state;
}
case "RemoveChannel": {
const channelsCopy = { ...state.channels };
delete channelsCopy[action.payload];
let newActive = { id: "", name: "", type: "channel" } as ChannelData;
if (Object.values(channelsCopy).length > 0) {
newActive = Object.values(channelsCopy)[0];
}
return { channels: channelsCopy, activeChannel: newActive };
}
default:
throw new Error();
}
}
export function useMessenger(
communityKey: string,
identity: Identity | undefined,
newNickname: string | undefined
) {
const [channelsState, channelsDispatch] = useReducer(channelReducer, {
channels: {},
activeChannel: { id: "", name: "", type: "channel" },
} as ChannelsState);
const [channelsState, channelsDispatch] = useChannelsReducer();
const messenger = useCreateMessenger(identity);
const { contacts, setContacts, contactsClass, nickname } = useContacts(
const { contacts, contactsDispatch, contactsClass, nickname } = useContacts(
messenger,
identity,
newNickname
@ -224,18 +154,21 @@ export function useMessenger(
Object.values(channelsState.channels)
.filter((channel) => channel.type === "dm")
.forEach((channel) => {
const contact = contacts?.[channel?.members?.[0]?.id ?? ""];
if (contact && channel.name !== (contact?.customName ?? channel.name)) {
const contact = contacts?.[channel?.members?.[1]?.id ?? ""];
if (
contact &&
channel.name !== (contact?.customName ?? contact.trueName)
) {
channelsDispatch({
type: "AddChannel",
payload: {
...channel,
name: contact?.customName ?? channel.name,
name: contact?.customName ?? contact.trueName,
},
});
}
});
}, [contacts]);
}, [contacts, channelsState.channels]);
const {
groupChat,
@ -276,7 +209,7 @@ export function useMessenger(
};
}
if (content) {
if (channelsState.activeChannel.type === "group") {
if (channelsState.activeChannel.type !== "channel") {
await groupChat?.sendMessage(
channelsState.activeChannel.id,
content,
@ -318,7 +251,7 @@ export function useMessenger(
loadingMessenger,
communityData,
contacts,
setContacts,
contactsDispatch,
channels: channelsState.channels,
channelsDispatch,
removeChannel,

View File

@ -1,60 +0,0 @@
import { useCallback, useMemo } from "react";
import { useMessengerContext } from "../contexts/messengerProvider";
export function useManageContact(id: string) {
const { contacts, setContacts } = useMessengerContext();
const contact = useMemo(() => contacts[id], [id, contacts]);
const setCustomName = useCallback(
(customName: string | undefined) => {
setContacts((prev) => {
const prevUser = prev[id];
if (!prevUser) return prev;
return { ...prev, [id]: { ...prevUser, customName } };
});
},
[id]
);
const setBlocked = useCallback(
(blocked: boolean) => {
setContacts((prev) => {
const prevUser = prev[id];
if (!prevUser) return prev;
return { ...prev, [id]: { ...prevUser, blocked } };
});
},
[id]
);
const setIsUntrustworthy = useCallback(
(isUntrustworthy: boolean) => {
setContacts((prev) => {
const prevUser = prev[id];
if (!prevUser) return prev;
return { ...prev, [id]: { ...prevUser, isUntrustworthy } };
});
},
[id]
);
const setIsUserFriend = useCallback(
(isFriend: boolean) => {
setContacts((prev) => {
const prevUser = prev[id];
if (!prevUser) return prev;
return { ...prev, [id]: { ...prevUser, isFriend } };
});
},
[id]
);
return {
contact,
setCustomName,
setBlocked,
setIsUntrustworthy,
setIsUserFriend,
};
}

View File

@ -32,12 +32,34 @@ export type GroupChatsType = {
};
/* TODO: add chat messages encryption */
class GroupChatUsers {
private users: { [id: string]: GroupMember } = {};
private identity: Identity;
public constructor(_identity: Identity) {
this.identity = _identity;
}
public async getUser(id: string): Promise<GroupMember> {
if (this.users[id]) {
return this.users[id];
}
const topic = await getNegotiatedTopic(this.identity, id);
const symKey = await createSymKeyFromPassword(topic);
const partitionedTopic = getPartitionedTopic(id);
const groupUser: GroupMember = { topic, symKey, id, partitionedTopic };
this.users[id] = groupUser;
return groupUser;
}
}
export class GroupChats {
waku: Waku;
identity: Identity;
private callback: (chats: GroupChat) => void;
private removeCallback: (chats: GroupChat) => void;
private addMessage: (message: ChatMessage, sender: string) => void;
private groupChatUsers;
public chats: GroupChatsType = {};
/**
@ -62,6 +84,7 @@ export class GroupChats {
) {
this.waku = waku;
this.identity = identity;
this.groupChatUsers = new GroupChatUsers(identity);
this.callback = callback;
this.removeCallback = removeCallback;
this.addMessage = addMessage;
@ -117,10 +140,7 @@ export class GroupChats {
const members: GroupMember[] = [];
await Promise.all(
event.event.members.map(async (member) => {
const topic = await getNegotiatedTopic(this.identity, member);
const symKey = await createSymKeyFromPassword(topic);
const partitionedTopic = getPartitionedTopic(member);
members.push({ topic, symKey, id: member, partitionedTopic });
members.push(await this.groupChatUsers.getUser(member));
})
);
await this.addChat(
@ -160,10 +180,7 @@ export class GroupChats {
const members: GroupMember[] = [];
await Promise.all(
event.event.members.map(async (member) => {
const topic = await getNegotiatedTopic(this.identity, member);
const symKey = await createSymKeyFromPassword(topic);
const partitionedTopic = getPartitionedTopic(member);
members.push({ topic, symKey, id: member, partitionedTopic });
members.push(await this.groupChatUsers.getUser(member));
})
);
chat.members.push(...members);
@ -353,10 +370,7 @@ export class GroupChats {
!chat.members.map((chatMember) => chatMember.id).includes(member)
)
.map(async (member) => {
const topic = await getNegotiatedTopic(this.identity, member);
const symKey = await createSymKeyFromPassword(topic);
const partitionedTopic = getPartitionedTopic(member);
newMembers.push({ topic, symKey, id: member, partitionedTopic });
newMembers.push(await this.groupChatUsers.getUser(member));
})
);
@ -383,10 +397,7 @@ export class GroupChats {
await Promise.all(
members.map(async (member) => {
const topic = await getNegotiatedTopic(this.identity, member);
const symKey = await createSymKeyFromPassword(topic);
const partitionedTopic = getPartitionedTopic(member);
newMembers.push({ topic, symKey, id: member, partitionedTopic });
newMembers.push(await this.groupChatUsers.getUser(member));
})
);