Improve profile modal (#132)

This commit is contained in:
Szymon Szlachtowicz 2021-11-18 16:34:26 +01:00 committed by GitHub
parent 7fb0bfbdc7
commit 81a14fcb9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 261 additions and 233 deletions

View File

@ -76,7 +76,7 @@ function DragDiv() {
<ReactChat <ReactChat
theme={theme ? lightTheme : darkTheme} theme={theme ? lightTheme : darkTheme}
communityKey={ communityKey={
"0x02f04996fd22b82e7b02a2d3bf07fa986c5181cd23190a992187d9c887011d4a8e" "0x0229d71fb62c14ec0370d2507ef7e13f1d53ddf1170db3babb5710fd113d0d94f9"
} }
fetchMetadata={fetchMetadata} fetchMetadata={fetchMetadata}
/> />

View File

@ -10,6 +10,7 @@ import { Community } from "./Community";
import { Members } from "./Members/Members"; import { Members } from "./Members/Members";
import { CommunityModal } from "./Modals/CommunityModal"; import { CommunityModal } from "./Modals/CommunityModal";
import { EditModal } from "./Modals/EditModal"; import { EditModal } from "./Modals/EditModal";
import { ProfileModal } from "./Modals/ProfileModal";
export function Chat() { export function Chat() {
const [showMembers, setShowMembers] = useState(true); const [showMembers, setShowMembers] = useState(true);
@ -55,6 +56,7 @@ export function Chat() {
)} )}
<CommunityModal subtitle="Public Community" /> <CommunityModal subtitle="Public Community" />
<EditModal /> <EditModal />
<ProfileModal />
</ChatWrapper> </ChatWrapper>
); );
} }

View File

@ -123,7 +123,7 @@ export function ChatCreation({
<Contacts> <Contacts>
<ContactsHeading>Contacts</ContactsHeading> <ContactsHeading>Contacts</ContactsHeading>
<ContactsList> <ContactsList>
{contacts {Object.values(contacts)
.filter( .filter(
(e) => (e) =>
e.id != bufToHex(identity.publicKey) && e.id != bufToHex(identity.publicKey) &&
@ -132,7 +132,7 @@ export function ChatCreation({
.map((contact) => ( .map((contact) => (
<Member <Member
key={contact.id} key={contact.id}
member={contact.id} contact={contact}
isOnline={contact.online} isOnline={contact.online}
onClick={() => addMember(contact.id)} onClick={() => addMember(contact.id)}
/> />

View File

@ -3,10 +3,29 @@ import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useFetchMetadata } from "../../contexts/fetchMetadataProvider"; import { useFetchMetadata } from "../../contexts/fetchMetadataProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { Metadata } from "../../models/Metadata"; import { Metadata } from "../../models/Metadata";
import { ContactMenu } from "../Form/ContactMenu";
import { ImageMenu } from "../Form/ImageMenu"; import { ImageMenu } from "../Form/ImageMenu";
interface MentionProps {
id: string;
}
function Mention({ id }: MentionProps) {
const { contacts } = useMessengerContext();
const contact = useMemo(() => contacts[id.slice(1)], [id, contacts]);
const [showMenu, setShowMenu] = useState(false);
if (!contact) return <>{id}</>;
return (
<MentionSpan onClick={() => setShowMenu(!showMenu)}>
{`@${contact.customName ?? contact.id}`}
{showMenu && <ContactMenu id={id.slice(1)} setShowMenu={setShowMenu} />}
</MentionSpan>
);
}
type ChatMessageContentProps = { type ChatMessageContentProps = {
message: ChatMessage; message: ChatMessage;
setImage: (image: string) => void; setImage: (image: string) => void;
@ -40,7 +59,7 @@ export function ChatMessageContent({
]; ];
} }
if (element.startsWith("@")) { if (element.startsWith("@")) {
return [<Mention key={idx}>{element}</Mention>, " "]; return [<Mention key={idx} id={element} />, " "];
} }
return [element, " "]; return [element, " "];
}); });
@ -158,9 +177,10 @@ const ContentWrapper = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const Mention = styled.span` const MentionSpan = styled.span`
color: blue; color: blue;
font-weight: 500; font-weight: 500;
position: relative;
`; `;
const Link = styled.a` const Link = styled.a`

View File

@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useBlockedUsers } from "../../contexts/blockedUsersProvider";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider"; import { useModal } from "../../contexts/modalProvider";
import { useChatScrollHandle } from "../../hooks/useChatScrollHandle"; import { useChatScrollHandle } from "../../hooks/useChatScrollHandle";
@ -14,7 +13,6 @@ import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
import { LinkModal, LinkModalName } from "../Modals/LinkModal"; import { LinkModal, LinkModalName } from "../Modals/LinkModal";
import { PictureModal, PictureModalName } from "../Modals/PictureModal"; import { PictureModal, PictureModalName } from "../Modals/PictureModal";
import { ProfileModal } from "../Modals/ProfileModal";
import { textMediumStyles, textSmallStyles } from "../Text"; import { textMediumStyles, textSmallStyles } from "../Text";
import { ChatMessageContent } from "./ChatMessageContent"; import { ChatMessageContent } from "./ChatMessageContent";
@ -27,10 +25,6 @@ type ChatUiMessageProps = {
prevMessage: ChatMessage; prevMessage: ChatMessage;
setImage: (img: string) => void; setImage: (img: string) => void;
setLink: (link: string) => void; setLink: (link: string) => void;
setUser: (user: string) => void;
customName?: string;
trueName?: string;
setRenaming: (val: boolean) => void;
}; };
function ChatUiMessage({ function ChatUiMessage({
@ -39,13 +33,13 @@ function ChatUiMessage({
prevMessage, prevMessage,
setImage, setImage,
setLink, setLink,
setUser,
customName,
trueName,
setRenaming,
}: ChatUiMessageProps) { }: ChatUiMessageProps) {
const { contacts } = useMessengerContext();
const contact = useMemo(
() => contacts[message.sender],
[message.sender, contacts]
);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [isUntrustworthy, setIsUntrustworthy] = useState(false);
return ( return (
<MessageOuterWrapper> <MessageOuterWrapper>
@ -60,19 +54,10 @@ function ChatUiMessage({
<Icon <Icon
onClick={() => { onClick={() => {
setShowMenu((e) => !e); setShowMenu((e) => !e);
setUser(message.sender);
}} }}
> >
{showMenu && ( {showMenu && (
<ContactMenu <ContactMenu id={message.sender} setShowMenu={setShowMenu} />
message={message}
setShowMenu={setShowMenu}
isUntrustworthy={isUntrustworthy}
setIsUntrustworthy={setIsUntrustworthy}
customName={customName}
trueName={trueName}
setRenaming={setRenaming}
/>
)} )}
<UserIcon /> <UserIcon />
</Icon> </Icon>
@ -82,14 +67,14 @@ function ChatUiMessage({
<UserNameWrapper> <UserNameWrapper>
<UserName> <UserName>
{" "} {" "}
{customName ? customName : message.sender.slice(0, 10)} {contact.customName ?? message.sender.slice(0, 10)}
</UserName> </UserName>
{customName && ( {contact.customName && (
<UserAddress> <UserAddress>
{message.sender.slice(0, 5)}...{message.sender.slice(-3)} {message.sender.slice(0, 5)}...{message.sender.slice(-3)}
</UserAddress> </UserAddress>
)} )}
{isUntrustworthy && <UntrustworthIcon />} {contact.isUntrustworthy && <UntrustworthIcon />}
</UserNameWrapper> </UserNameWrapper>
<TimeWrapper>{message.date.toLocaleString()}</TimeWrapper> <TimeWrapper>{message.date.toLocaleString()}</TimeWrapper>
</MessageHeaderWrapper> </MessageHeaderWrapper>
@ -107,20 +92,20 @@ function ChatUiMessage({
} }
export function ChatMessages() { export function ChatMessages() {
const { messages, activeChannel } = useMessengerContext(); const { messages, activeChannel, contacts } = useMessengerContext();
const ref = useRef<HTMLHeadingElement>(null); const ref = useRef<HTMLHeadingElement>(null);
const loadingMessages = useChatScrollHandle(messages, ref, activeChannel.id); const loadingMessages = useChatScrollHandle(messages, ref, activeChannel.id);
const { blockedUsers } = useBlockedUsers();
const shownMessages = useMemo( const shownMessages = useMemo(
() => messages.filter((message) => !blockedUsers.includes(message.sender)), () =>
[blockedUsers, messages, messages.length] messages.filter(
(message) => !contacts?.[message.sender]?.blocked ?? true
),
[contacts, messages, messages.length]
); );
const [image, setImage] = useState(""); const [image, setImage] = useState("");
const [link, setLink] = useState(""); const [link, setLink] = useState("");
const [user, setUser] = useState("");
const { setModal: setPictureModal, isVisible: showPictureModal } = const { setModal: setPictureModal, isVisible: showPictureModal } =
useModal(PictureModalName); useModal(PictureModalName);
@ -136,23 +121,10 @@ export function ChatMessages() {
); );
useEffect(() => (!showLinkModal ? setLink("") : undefined), [showLinkModal]); useEffect(() => (!showLinkModal ? setLink("") : undefined), [showLinkModal]);
const [renaming, setRenaming] = useState(false);
const [customName, setCustomName] = useState("");
const [trueName, setTrueName] = useState("");
return ( return (
<MessagesWrapper ref={ref}> <MessagesWrapper ref={ref}>
<PictureModal image={image} /> <PictureModal image={image} />
<LinkModal link={link} /> <LinkModal link={link} />
<ProfileModal
user={user}
renaming={renaming}
setRenaming={setRenaming}
customName={customName}
setCustomName={setCustomName}
trueName={trueName}
setTrueName={setTrueName}
/>
<EmptyChannel channel={activeChannel} /> <EmptyChannel channel={activeChannel} />
{loadingMessages && ( {loadingMessages && (
<LoadingWrapper> <LoadingWrapper>
@ -167,10 +139,6 @@ export function ChatMessages() {
prevMessage={shownMessages[idx - 1]} prevMessage={shownMessages[idx - 1]}
setLink={setLink} setLink={setLink}
setImage={setImage} setImage={setImage}
setUser={setUser}
customName={customName}
trueName={trueName}
setRenaming={setRenaming}
/> />
))} ))}
</MessagesWrapper> </MessagesWrapper>

View File

@ -1,11 +1,12 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components"; import styled from "styled-components";
import { useBlockedUsers } from "../../contexts/blockedUsersProvider";
import { useFriends } from "../../contexts/friendsProvider"; import { useFriends } from "../../contexts/friendsProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider"; import { useModal } from "../../contexts/modalProvider";
import { ChatMessage } from "../../models/ChatMessage"; import { useManageContact } from "../../hooks/useManageContact";
import { Icon, UserAddress } from "../Chat/ChatMessages"; import { UserAddress } from "../Chat/ChatMessages";
import { AddContactSvg } from "../Icons/AddContactIcon"; import { AddContactSvg } from "../Icons/AddContactIcon";
import { BlockSvg } from "../Icons/BlockIcon"; import { BlockSvg } from "../Icons/BlockIcon";
import { ChatSvg } from "../Icons/ChatIcon"; import { ChatSvg } from "../Icons/ChatIcon";
@ -20,63 +21,45 @@ import { textMediumStyles } from "../Text";
import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu"; import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu";
type ContactMenuProps = { type ContactMenuProps = {
message: ChatMessage; id: string;
setShowMenu: (val: boolean) => void; setShowMenu: (val: boolean) => void;
isUntrustworthy: boolean;
setIsUntrustworthy: (val: boolean) => void;
customName?: string;
trueName?: string;
setRenaming: (val: boolean) => void;
}; };
export function ContactMenu({ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
message, const identity = useIdentity();
setShowMenu, const isUser = useMemo(
isUntrustworthy, () => id === bufToHex(identity.publicKey),
setIsUntrustworthy, [id, identity]
customName,
trueName,
setRenaming,
}: ContactMenuProps) {
const id = message.sender;
const { blockedUsers, setBlockedUsers } = useBlockedUsers();
const userInBlocked = useMemo(
() => blockedUsers.includes(id),
[blockedUsers, id]
); );
const { setModal } = useModal(ProfileModalName);
const { friends, setFriends } = useFriends(); const { friends, setFriends } = useFriends();
const userIsFriend = useMemo(() => friends.includes(id), [friends, id]); const userIsFriend = useMemo(() => friends.includes(id), [friends, id]);
const { contact, setBlocked, setIsUntrustworthy } = useManageContact(id);
const { setModal } = useModal(ProfileModalName); if (!contact) return null;
return ( return (
<ContactDropdown> <ContactDropdown>
<ContactInfo> <ContactInfo>
{message.image ? ( <UserIcon />
<Icon
style={{
backgroundImage: `url(${message.image}`,
}}
/>
) : (
<UserIcon />
)}
<UserNameWrapper> <UserNameWrapper>
<UserName> <UserName>{contact.customName ?? id.slice(0, 10)}</UserName>
{customName ? customName : message.sender.slice(0, 10)} {contact.isUntrustworthy && <UntrustworthIcon />}
</UserName>
{isUntrustworthy && <UntrustworthIcon />}
</UserNameWrapper> </UserNameWrapper>
{trueName && <UserTrueName>({trueName})</UserTrueName>} {contact.customName && (
<UserTrueName>({contact.trueName})</UserTrueName>
)}
<UserAddress> <UserAddress>
{message.sender.slice(0, 10)}...{message.sender.slice(-3)} {id.slice(0, 10)}...{id.slice(-3)}
</UserAddress> </UserAddress>
</ContactInfo> </ContactInfo>
<MenuSection> <MenuSection>
<MenuItem onClick={() => setModal(true)}> <MenuItem
onClick={() => {
setModal({ id, renamingState: false });
}}
>
<ProfileSvg width={16} height={16} /> <ProfileSvg width={16} height={16} />
<MenuText>View Profile</MenuText> <MenuText>View Profile</MenuText>
</MenuItem> </MenuItem>
@ -94,8 +77,7 @@ export function ContactMenu({
)} )}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
setModal(true); setModal({ id, renamingState: true });
setRenaming(true);
}} }}
> >
<EditSvg width={16} height={16} /> <EditSvg width={16} height={16} />
@ -103,30 +85,30 @@ export function ContactMenu({
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
<MenuSection> <MenuSection>
<MenuItem onClick={() => setIsUntrustworthy(!isUntrustworthy)}> <MenuItem onClick={() => setIsUntrustworthy(!contact.isUntrustworthy)}>
<WarningSvg <WarningSvg
width={16} width={16}
height={16} height={16}
className={isUntrustworthy ? "" : "red"} className={contact.isUntrustworthy ? "" : "red"}
/> />
<MenuText className={isUntrustworthy ? "" : "red"}> <MenuText className={contact.isUntrustworthy ? "" : "red"}>
{isUntrustworthy {contact.isUntrustworthy
? "Remove Untrustworthy Mark" ? "Remove Untrustworthy Mark"
: "Mark as Untrustworthy"} : "Mark as Untrustworthy"}
</MenuText> </MenuText>
</MenuItem> </MenuItem>
{!userIsFriend && ( {!userIsFriend && !isUser && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
userInBlocked setBlocked(!contact.blocked);
? setBlockedUsers((prev) => prev.filter((e) => e != id))
: setBlockedUsers((prev) => [...prev, id]);
setShowMenu(false); setShowMenu(false);
}} }}
> >
<BlockSvg width={16} height={16} className="red" /> <BlockSvg width={16} height={16} className="red" />
<MenuText className="red">Block User</MenuText> <MenuText className="red">
{contact.blocked ? "Unblock User" : "Block User"}
</MenuText>
</MenuItem> </MenuItem>
)} )}
</MenuSection> </MenuSection>

View File

@ -1,12 +1,13 @@
import React from "react"; import React, { useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Contact } from "../../models/Contact";
import { Icon } from "../Chat/ChatMessages"; import { Icon } from "../Chat/ChatMessages";
// import { ContactMenu } from '../Form/ContactMenu'; import { ContactMenu } from "../Form/ContactMenu";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
interface MemberProps { interface MemberProps {
member: string; contact: Contact;
isOnline?: boolean; isOnline?: boolean;
switchShowMembers?: () => void; switchShowMembers?: () => void;
setMembersList?: any; setMembersList?: any;
@ -14,7 +15,7 @@ interface MemberProps {
} }
export function Member({ export function Member({
member, contact,
isOnline, isOnline,
switchShowMembers, switchShowMembers,
setMembersList, setMembersList,
@ -30,11 +31,11 @@ export function Member({
}); });
}; };
// const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const onMemberClick = () => { const onMemberClick = () => {
switchShowMembers?.(); switchShowMembers?.();
startDialog(member); startDialog(contact.id);
}; };
return ( return (
@ -44,12 +45,12 @@ export function Member({
backgroundImage: "unset", backgroundImage: "unset",
}} }}
className={isOnline ? "online" : "offline"} className={isOnline ? "online" : "offline"}
// onClick={() => setShowMenu(e => !e)} onClick={() => setShowMenu((e) => !e)}
> >
{/* {showMenu && <ContactMenu id={member} setShowMenu={setShowMenu} />} */} {showMenu && <ContactMenu id={contact.id} setShowMenu={setShowMenu} />}
<UserIcon memberView={true} /> <UserIcon memberView={true} />
</MemberIcon> </MemberIcon>
<MemberName>{member}</MemberName> <MemberName>{contact.customName ?? contact.id}</MemberName>
</MemberData> </MemberData>
); );
} }

View File

@ -34,13 +34,13 @@ export function MembersList({
</MemberCategory> </MemberCategory>
<MemberCategory> <MemberCategory>
<MemberCategoryName>Online</MemberCategoryName> <MemberCategoryName>Online</MemberCategoryName>
{contacts {Object.values(contacts)
.filter((e) => e.id != bufToHex(identity.publicKey)) .filter((e) => e.id != bufToHex(identity.publicKey))
.filter((e) => e.online) .filter((e) => e.online)
.map((contact) => ( .map((contact) => (
<Member <Member
key={contact.id} key={contact.id}
member={contact.id} contact={contact}
isOnline={contact.online} isOnline={contact.online}
switchShowMembers={switchShowMembers} switchShowMembers={switchShowMembers}
setMembersList={setMembersList} setMembersList={setMembersList}
@ -49,13 +49,13 @@ export function MembersList({
</MemberCategory> </MemberCategory>
<MemberCategory> <MemberCategory>
<MemberCategoryName>Offline</MemberCategoryName> <MemberCategoryName>Offline</MemberCategoryName>
{contacts {Object.values(contacts)
.filter((e) => e.id != bufToHex(identity.publicKey)) .filter((e) => e.id != bufToHex(identity.publicKey))
.filter((e) => !e.online) .filter((e) => !e.online)
.map((contact) => ( .map((contact) => (
<Member <Member
key={contact.id} key={contact.id}
member={contact.id} contact={contact}
isOnline={contact.online} isOnline={contact.online}
switchShowMembers={switchShowMembers} switchShowMembers={switchShowMembers}
setMembersList={setMembersList} setMembersList={setMembersList}

View File

@ -3,9 +3,9 @@ import styled from "styled-components";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
export const PictureModalName = "PictureModal"; export const PictureModalName = "PictureModal" as const;
interface PictureModalProps { export interface PictureModalProps {
image: string; image: string;
} }

View File

@ -1,8 +1,11 @@
import React, { useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components"; import styled from "styled-components";
import { useBlockedUsers } from "../../contexts/blockedUsersProvider";
import { useFriends } from "../../contexts/friendsProvider"; import { useFriends } from "../../contexts/friendsProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
import { copy } from "../../utils"; import { copy } from "../../utils";
import { buttonStyles } from "../Buttons/buttonStyle"; import { buttonStyles } from "../Buttons/buttonStyle";
import { ClearSvg } from "../Icons/ClearIcon"; import { ClearSvg } from "../Icons/ClearIcon";
@ -16,46 +19,45 @@ import { textMediumStyles } from "../Text";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { ButtonSection, Heading, Section } from "./ModalStyle"; import { ButtonSection, Heading, Section } from "./ModalStyle";
export const ProfileModalName = "profileModal"; export const ProfileModalName = "profileModal" as const;
interface ProfileModalProps { export type ProfileModalProps = {
user: string; id: string;
image?: string; image?: string;
renaming: boolean; renamingState?: boolean;
customName?: string; };
trueName?: string;
setRenaming: (val: boolean) => void;
setTrueName: (val: string) => void;
setCustomName: (val: string) => void;
}
export const ProfileModal = ({ export const ProfileModal = () => {
user, const { props } = useModal(ProfileModalName);
image, const { id, image, renamingState } = useMemo(
renaming, () => (props ? props : { id: "" }),
customName, [props]
trueName, );
setRenaming,
setTrueName,
setCustomName,
}: ProfileModalProps) => {
const [isUntrustworthy, setIsUntrustworthy] = useState(false);
const { blockedUsers, setBlockedUsers } = useBlockedUsers(); const identity = useIdentity();
const isUser = useMemo(
const userInBlocked = useMemo( () => id === bufToHex(identity.publicKey),
() => blockedUsers.includes(user), [id, identity]
[blockedUsers, user]
); );
const { friends, setFriends } = useFriends(); const { friends, setFriends } = useFriends();
const userIsFriend = useMemo(() => friends.includes(user), [friends, user]); const userIsFriend = useMemo(() => friends.includes(id), [friends, id]);
const [renaming, setRenaming] = useState(renamingState ?? false);
useEffect(() => {
setRenaming(renamingState ?? false);
}, [renamingState]);
const { contact, setBlocked, setCustomName, setIsUntrustworthy } =
useManageContact(id);
const [customNameInput, setCustomNameInput] = useState("");
if (!contact) return null;
return ( return (
<Modal name={ProfileModalName} className="profile"> <Modal name={ProfileModalName} className="profile">
<Section> <Section>
<Heading>{user.slice(0, 10)}s Profile</Heading> <Heading>{id.slice(0, 10)}s Profile</Heading>
</Section> </Section>
<ProfileSection> <ProfileSection>
@ -70,8 +72,8 @@ export const ProfileModal = ({
<UserIcon /> <UserIcon />
)} )}
<UserNameWrapper> <UserNameWrapper>
<UserName>{customName ? customName : user.slice(0, 10)}</UserName> <UserName>{contact.customName ?? id.slice(0, 10)}</UserName>
{isUntrustworthy && <UntrustworthIcon />} {contact.isUntrustworthy && <UntrustworthIcon />}
{!renaming && ( {!renaming && (
<button onClick={() => setRenaming(true)}> <button onClick={() => setRenaming(true)}>
{" "} {" "}
@ -79,21 +81,22 @@ export const ProfileModal = ({
</button> </button>
)} )}
</UserNameWrapper> </UserNameWrapper>
{trueName && <UserTrueName>{trueName}</UserTrueName>} {contact.customName && (
{trueName && <button onClick={() => setTrueName("")}></button>} <UserTrueName>{contact.trueName}</UserTrueName>
)}
</NameSection> </NameSection>
{renaming ? ( {renaming ? (
<NameInputWrapper> <NameInputWrapper>
<NameInput <NameInput
placeholder="Only you will see this nickname" placeholder="Only you will see this nickname"
value={customName} value={contact.customName}
onChange={(e) => setCustomName(e.currentTarget.value)} onChange={(e) => setCustomNameInput(e.currentTarget.value)}
/> />
{customName && ( {contact.customName && (
<ClearBtn <ClearBtn
onClick={() => { onClick={() => {
setCustomName(""); setCustomName(undefined);
setTrueName(""); setCustomNameInput("");
}} }}
> >
<ClearSvg width={16} height={16} className="profile" /> <ClearSvg width={16} height={16} className="profile" />
@ -103,9 +106,9 @@ export const ProfileModal = ({
) : ( ) : (
<> <>
<UserAddressWrapper> <UserAddressWrapper>
<UserAddress>Chatkey: {user.slice(0, 30)}</UserAddress> <UserAddress>Chatkey: {id.slice(0, 30)}</UserAddress>
<CopyButton onClick={() => copy(user)}> <CopyButton onClick={() => copy(id)}>
<CopySvg width={24} height={24} /> <CopySvg width={24} height={24} />
</CopyButton> </CopyButton>
</UserAddressWrapper> </UserAddressWrapper>
@ -120,9 +123,9 @@ export const ProfileModal = ({
<LeftIconSvg width={28} height={28} /> <LeftIconSvg width={28} height={28} />
</BackBtn> </BackBtn>
<Btn <Btn
disabled={!customName} disabled={!customNameInput}
onClick={() => { onClick={() => {
setTrueName(user.slice(0, 10)); setCustomName(customNameInput);
setRenaming(false); setRenaming(false);
}} }}
> >
@ -131,38 +134,36 @@ export const ProfileModal = ({
</> </>
) : ( ) : (
<> <>
{!userIsFriend && ( {!userIsFriend && !isUser && (
<ProfileBtn <ProfileBtn
className={userInBlocked ? "" : "red"} className={contact.blocked ? "" : "red"}
onClick={() => { onClick={() => {
userInBlocked setBlocked(!contact.blocked);
? setBlockedUsers((prev) => prev.filter((e) => e != user))
: setBlockedUsers((prev) => [...prev, user]);
}} }}
> >
{userInBlocked ? "Unblock" : "Block"} {contact.blocked ? "Unblock" : "Block"}
</ProfileBtn> </ProfileBtn>
)} )}
{userIsFriend && ( {userIsFriend && (
<ProfileBtn <ProfileBtn
className="red" className="red"
onClick={() => onClick={() =>
setFriends((prev) => prev.filter((e) => e != user)) setFriends((prev) => prev.filter((e) => e != id))
} }
> >
Remove Contact Remove Contact
</ProfileBtn> </ProfileBtn>
)} )}
<ProfileBtn <ProfileBtn
className={isUntrustworthy ? "" : "red"} className={contact.isUntrustworthy ? "" : "red"}
onClick={() => setIsUntrustworthy(!isUntrustworthy)} onClick={() => setIsUntrustworthy(!contact.isUntrustworthy)}
> >
{isUntrustworthy {contact.isUntrustworthy
? "Remove Untrustworthy Mark" ? "Remove Untrustworthy Mark"
: "Mark as Untrustworthy"} : "Mark as Untrustworthy"}
</ProfileBtn> </ProfileBtn>
{!userIsFriend && ( {!userIsFriend && (
<Btn onClick={() => setFriends((prev) => [...prev, user])}> <Btn onClick={() => setFriends((prev) => [...prev, id])}>
Send Contact Request Send Contact Request
</Btn> </Btn>
)} )}

View File

@ -2,7 +2,6 @@ import React, { useRef } from "react";
import { ThemeProvider } from "styled-components"; import { ThemeProvider } from "styled-components";
import styled from "styled-components"; import styled from "styled-components";
import { BlockedUsersProvider } from "../contexts/blockedUsersProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider"; import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { FriendsProvider } from "../contexts/friendsProvider"; import { FriendsProvider } from "../contexts/friendsProvider";
import { ModalProvider } from "../contexts/modalProvider"; import { ModalProvider } from "../contexts/modalProvider";
@ -29,17 +28,15 @@ export function ReactChat({
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NarrowProvider myRef={ref}> <NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}> <FetchMetadataProvider fetchMetadata={fetchMetadata}>
<BlockedUsersProvider> <FriendsProvider>
<FriendsProvider> <ModalProvider>
<ModalProvider> <Wrapper ref={ref}>
<Wrapper ref={ref}> <GlobalStyle />
<GlobalStyle /> <ChatLoader communityKey={communityKey} />
<ChatLoader communityKey={communityKey} /> <div id="modal-root" />
<div id="modal-root" /> </Wrapper>
</Wrapper> </ModalProvider>
</ModalProvider> </FriendsProvider>
</FriendsProvider>
</BlockedUsersProvider>
</FetchMetadataProvider> </FetchMetadataProvider>
</NarrowProvider> </NarrowProvider>
</ThemeProvider> </ThemeProvider>

View File

@ -22,7 +22,7 @@ export const SearchBlock = ({
const { contacts } = useMessengerContext(); const { contacts } = useMessengerContext();
const searchList = useMemo(() => { const searchList = useMemo(() => {
return contacts return Object.values(contacts)
.filter((member) => member.id.includes(query)) .filter((member) => member.id.includes(query))
.filter((member) => !dsicludeList.includes(member.id)); .filter((member) => !dsicludeList.includes(member.id));
}, [query, dsicludeList, contacts]); }, [query, dsicludeList, contacts]);
@ -35,7 +35,7 @@ export const SearchBlock = ({
style={{ [onBotttom ? "bottom" : "top"]: "calc(100% + 24px)" }} style={{ [onBotttom ? "bottom" : "top"]: "calc(100% + 24px)" }}
> >
<ContactsList> <ContactsList>
{contacts {Object.values(contacts)
.filter((member) => member.id.includes(query)) .filter((member) => member.id.includes(query))
.filter((member) => !dsicludeList.includes(member.id)) .filter((member) => !dsicludeList.includes(member.id))
.map((member) => ( .map((member) => (

View File

@ -1,27 +0,0 @@
import React, { createContext, useContext, useState } from "react";
const BlockedUsersContext = createContext<{
blockedUsers: string[];
setBlockedUsers: React.Dispatch<React.SetStateAction<string[]>>;
}>({
blockedUsers: [],
setBlockedUsers: () => undefined,
});
export function useBlockedUsers() {
return useContext(BlockedUsersContext);
}
interface BlockedUsersProviderProps {
children: React.ReactNode;
}
export function BlockedUsersProvider({ children }: BlockedUsersProviderProps) {
const [blockedUsers, setBlockedUsers] = useState<string[]>([]);
return (
<BlockedUsersContext.Provider
value={{ blockedUsers, setBlockedUsers }}
children={children}
/>
);
}

View File

@ -14,7 +14,8 @@ const MessengerContext = createContext<MessengerType>({
loadPrevDay: async () => undefined, loadPrevDay: async () => undefined,
loadingMessages: false, loadingMessages: false,
communityData: undefined, communityData: undefined,
contacts: [], contacts: {},
setContacts: () => undefined,
activeChannel: { activeChannel: {
id: "", id: "",
name: "", name: "",

View File

@ -6,8 +6,17 @@ import React, {
useState, useState,
} from "react"; } from "react";
type ModalsState = { import {
[name: string]: boolean; ProfileModalName,
ProfileModalProps,
} from "../components/Modals/ProfileModal";
type TypeMap = {
[ProfileModalName]?: ProfileModalProps;
};
type ModalsState = TypeMap & {
[name: string]: boolean | undefined;
}; };
type ModalContextType = [ type ModalContextType = [
@ -17,11 +26,18 @@ type ModalContextType = [
const ModalContext = createContext<ModalContextType>([{}, () => undefined]); const ModalContext = createContext<ModalContextType>([{}, () => undefined]);
export function useModal(name: string) { export function useModal<T extends string>(name: T) {
const [modals, setModals] = useContext(ModalContext); const [modals, setModals] = useContext(ModalContext);
const setModal = useCallback( const setModal = useCallback(
(state: boolean) => { (state: T extends keyof TypeMap ? TypeMap[T] | false : boolean) => {
setModals((prev) => { setModals((prev) => {
if (!state) {
return {
...prev,
[name]: undefined,
};
}
return { return {
...prev, ...prev,
[name]: state, [name]: state,
@ -30,8 +46,11 @@ export function useModal(name: string) {
}, },
[name, modals] [name, modals]
); );
const isVisible = useMemo(() => modals?.[name] ?? false, [modals, name]); const isVisible = useMemo(() => !!modals?.[name], [modals, name]);
return { isVisible, setModal };
const props = useMemo(() => modals?.[name], [modals, name]);
return { isVisible, setModal, props };
} }
interface IdentityProviderProps { interface IdentityProviderProps {

View File

@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
Community, Community,
Contacts, Contacts as ContactsClass,
Identity, Identity,
Messenger, Messenger,
} from "status-communities/dist/cjs"; } from "status-communities/dist/cjs";
@ -10,7 +10,7 @@ import {
import { ChannelData } from "../../models/ChannelData"; import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { CommunityData } from "../../models/CommunityData"; import { CommunityData } from "../../models/CommunityData";
import { Contact } from "../../models/Contact"; import { Contacts } from "../../models/Contact";
import { createCommunity } from "../../utils/createCommunity"; import { createCommunity } from "../../utils/createCommunity";
import { createMessenger } from "../../utils/createMessenger"; import { createMessenger } from "../../utils/createMessenger";
import { uintToImgUrl } from "../../utils/uintToImgUrl"; import { uintToImgUrl } from "../../utils/uintToImgUrl";
@ -32,7 +32,8 @@ export type MessengerType = {
loadPrevDay: (id: string) => Promise<void>; loadPrevDay: (id: string) => Promise<void>;
loadingMessages: boolean; loadingMessages: boolean;
communityData: CommunityData | undefined; communityData: CommunityData | undefined;
contacts: Contact[]; contacts: Contacts;
setContacts: React.Dispatch<React.SetStateAction<Contacts>>;
channels: ChannelData[]; channels: ChannelData[];
activeChannel: ChannelData; activeChannel: ChannelData;
setActiveChannel: (channel: ChannelData) => void; setActiveChannel: (channel: ChannelData) => void;
@ -58,7 +59,7 @@ export function useMessenger(
const contactsClass = useMemo(() => { const contactsClass = useMemo(() => {
if (messenger && identity) { if (messenger && identity) {
const newContacts = new Contacts( const newContacts = new ContactsClass(
identity, identity,
messenger.waku, messenger.waku,
(id, clock) => { (id, clock) => {
@ -71,13 +72,25 @@ export function useMessenger(
} }
}, [messenger, identity]); }, [messenger, identity]);
const contacts = useMemo<Contact[]>(() => { const [contacts, setContacts] = useState<Contacts>({});
useEffect(() => {
const now = Date.now(); const now = Date.now();
return Object.entries(internalContacts).map(([id, clock]) => { setContacts((prev) => {
return { const newContacts: Contacts = {};
id, Object.entries(internalContacts).forEach(([id, clock]) => {
online: clock > now - 301000, newContacts[id] = {
}; id,
online: clock > now - 301000,
trueName: id.slice(0, 10),
isUntrustworthy: false,
blocked: false,
};
if (prev[id]) {
newContacts[id] = { ...prev[id], ...newContacts[id] };
}
});
return newContacts;
}); });
}, [internalContacts]); }, [internalContacts]);
@ -185,6 +198,7 @@ export function useMessenger(
loadingMessages, loadingMessages,
communityData, communityData,
contacts, contacts,
setContacts,
channels, channels,
activeChannel, activeChannel,
setActiveChannel, setActiveChannel,

View File

@ -0,0 +1,42 @@
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]
);
return { contact, setCustomName, setBlocked, setIsUntrustworthy };
}

View File

@ -1,4 +1,12 @@
export type Contact = { export type Contact = {
id: string; id: string;
online: boolean; online: boolean;
trueName: string;
customName?: string;
isUntrustworthy: boolean;
blocked: boolean;
};
export type Contacts = {
[id: string]: Contact;
}; };

View File

@ -6,7 +6,7 @@ import { StatusUpdate_StatusType } from "./proto/communities/v1/status_update";
import { bufToHex } from "./utils"; import { bufToHex } from "./utils";
import { StatusUpdate } from "./wire/status_update"; import { StatusUpdate } from "./wire/status_update";
const STATUS_BROADCAST_INTERVAL = 300000; const STATUS_BROADCAST_INTERVAL = 30000;
export class Contacts { export class Contacts {
waku: Waku; waku: Waku;