Refactor channels and providers (#134)

This commit is contained in:
Szymon Szlachtowicz 2021-11-19 09:31:49 +01:00 committed by GitHub
parent 7922343fa6
commit 4852d90546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 178 additions and 216 deletions

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React from "react";
import styled from "styled-components";
import { useNarrow } from "../../contexts/narrowProvider";
@ -7,12 +7,51 @@ import { GroupIcon } from "../Icons/GroupIcon";
import { MutedIcon } from "../Icons/MutedIcon";
import { textMediumStyles } from "../Text";
function RenderChannelName({
channel,
className,
}: {
channel: ChannelData;
className?: string;
}) {
switch (channel.type) {
case "group":
return (
<div className={className}>
<GroupIcon />
{channel.name}
</div>
);
case "channel":
return <div className={className}>{`# ${channel.name}`}</div>;
case "dm":
return <div className={className}>{channel.name}</div>;
}
}
function ChannelIcon({
channel,
activeView,
}: {
channel: ChannelData;
activeView?: boolean;
}) {
const narrow = useNarrow();
return (
<ChannelLogo
icon={channel.icon}
className={activeView ? "active" : narrow ? "narrow" : ""}
>
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
</ChannelLogo>
);
}
interface ChannelProps {
channel: ChannelData;
notification?: number;
notified?: boolean;
mention?: number;
isActive: boolean;
isMuted: boolean;
activeView?: boolean;
onClick?: () => void;
}
@ -20,75 +59,48 @@ interface ChannelProps {
export function Channel({
channel,
isActive,
isMuted,
activeView,
onClick,
notification,
notified,
mention,
}: ChannelProps) {
const narrow = useNarrow();
const className = useMemo(
() => (narrow && !activeView ? "narrow" : activeView ? "active" : ""),
[narrow]
);
return (
<ChannelWrapper
className={
(isActive && !activeView) || (isActive && narrow) ? "active" : ""
}
style={{ width: narrow && activeView ? "calc(100% - 162px)" : "100%" }}
className={`${isActive && "active"}`}
isNarrow={narrow && activeView}
onClick={onClick}
>
<ChannelInfo>
<ChannelLogo
style={{
backgroundImage: channel.icon ? `url(${channel.icon}` : "",
}}
className={className}
>
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
</ChannelLogo>
<ChannelIcon channel={channel} activeView={activeView} />
<ChannelTextInfo>
<ChannelName
className={
isActive || narrow
? "active"
: notification && notification > 0
? "notified"
: isMuted
? "muted"
: ""
}
>
{channel.type && channel.type === "group" ? (
<GroupIcon />
) : channel.type === "dm" ? (
""
) : (
"#"
)}{" "}
{channel.name}
</ChannelName>
channel={channel}
active={isActive || narrow}
muted={channel?.isMuted}
notified={notified}
/>
{activeView && (
<ChannelDescription>{channel.description}</ChannelDescription>
)}
</ChannelTextInfo>
</ChannelInfo>
{notification && notification > 0 && !activeView && mention && (
{!activeView && !!mention && mention > 0 && !channel?.isMuted && (
<NotificationBagde>{mention}</NotificationBagde>
)}
{isMuted && !notification && <MutedIcon />}
{channel?.isMuted && <MutedIcon />}
</ChannelWrapper>
);
}
const ChannelWrapper = styled.div`
const ChannelWrapper = styled.div<{ isNarrow?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
cursor: pointer;
width: ${({ isNarrow }) => (isNarrow ? "calc(100% - 162px)" : "100%")};
&.active {
background-color: ${({ theme }) => theme.activeChannelBackground};
border-radius: 8px;
@ -109,7 +121,7 @@ const ChannelTextInfo = styled.div`
white-space: nowrap;
`;
export const ChannelLogo = styled.div`
export const ChannelLogo = styled.div<{ icon?: string }>`
width: 24px;
height: 24px;
display: flex;
@ -124,45 +136,37 @@ export const ChannelLogo = styled.div`
background-color: ${({ theme }) => theme.iconColor};
background-size: cover;
background-repeat: no-repeat;
backgroundimage: ${({ icon }) => icon && `url(${icon}`};
color: ${({ theme }) => theme.iconTextColor};
&.active {
width: 36px;
height: 36px;
font-size: 20px;
line-height: 20px;
}
&.narrow {
width: 40px;
height: 40px;
font-size: 20px;
line-height: 20px;
}
`;
export const ChannelName = styled.div`
font-weight: 500;
opacity: 0.7;
export const ChannelName = styled(RenderChannelName)<{
muted?: boolean;
notified?: boolean;
active?: boolean;
}>`
font-weight: ${({ notified, muted, active }) =>
notified && !muted && !active ? "600" : "500"};
opacity: ${({ notified, muted, active }) =>
muted ? "0.4" : notified || active ? "1.0" : "0.7"};
color: ${({ theme }) => theme.primary};
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
${textMediumStyles}
&.active,
&.notified {
opacity: 1;
}
&.muted {
opacity: 0.4;
}
&.notified {
font-weight: 600;
}
`;
const ChannelDescription = styled.p`

View File

@ -1,13 +1,13 @@
import React, { useEffect } from "react";
import styled from "styled-components";
import { ChatState, useChatState } from "../../contexts/chatStateProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { CreateIcon } from "../Icons/CreateIcon";
import { Channel } from "./Channel";
interface ChannelsProps {
setCreateChat: (val: boolean) => void;
onCommunityClick?: () => void;
}
@ -15,13 +15,10 @@ type GenerateChannelsProps = ChannelsProps & {
type: string;
};
function GenerateChannels({
type,
onCommunityClick,
setCreateChat,
}: GenerateChannelsProps) {
function GenerateChannels({ type, onCommunityClick }: GenerateChannelsProps) {
const { mentions, notifications, activeChannel, setActiveChannel, channels } =
useMessengerContext();
const setChatState = useChatState()[1];
return (
<>
{Object.values(channels)
@ -31,23 +28,14 @@ function GenerateChannels({
key={channel.id}
channel={channel}
isActive={channel.id === activeChannel.id}
isMuted={channel.isMuted || false}
notification={
notifications[channel.id] > 0 && !channel.isMuted
? notifications[channel.id]
: undefined
}
mention={
mentions[channel.id] > 0 && !channel.isMuted
? mentions[channel.id]
: undefined
}
notified={notifications?.[channel.id] > 0}
mention={mentions?.[channel.id]}
onClick={() => {
setActiveChannel(channel);
if (onCommunityClick) {
onCommunityClick();
}
setCreateChat(false);
setChatState(ChatState.ChatBody);
}}
/>
))}
@ -55,7 +43,7 @@ function GenerateChannels({
);
}
export function Channels({ setCreateChat, onCommunityClick }: ChannelsProps) {
export function Channels({ onCommunityClick }: ChannelsProps) {
const { clearNotifications, clearMentions, notifications, activeChannel } =
useMessengerContext();
useEffect(() => {
@ -66,19 +54,15 @@ export function Channels({ setCreateChat, onCommunityClick }: ChannelsProps) {
}
}
}, [notifications, activeChannel]);
const setChatState = useChatState()[1];
return (
<ChannelList>
<GenerateChannels
type={"channel"}
onCommunityClick={onCommunityClick}
setCreateChat={setCreateChat}
/>
<GenerateChannels type={"channel"} onCommunityClick={onCommunityClick} />
<Chats>
<ChatsBar>
<Heading>Chat</Heading>
<EditBtn onClick={() => setCreateChat(true)}>
<EditBtn onClick={() => setChatState(ChatState.ChatCreation)}>
<CreateIcon />
</EditBtn>
</ChatsBar>
@ -86,13 +70,8 @@ export function Channels({ setCreateChat, onCommunityClick }: ChannelsProps) {
<GenerateChannels
type={"group"}
onCommunityClick={onCommunityClick}
setCreateChat={setCreateChat}
/>
<GenerateChannels
type={"dm"}
onCommunityClick={onCommunityClick}
setCreateChat={setCreateChat}
/>
<GenerateChannels type={"dm"} onCommunityClick={onCommunityClick} />
</ChatsList>
</Chats>
</ChannelList>

View File

@ -16,17 +16,11 @@ export function EmptyChannel({ channel }: EmptyChannelProps) {
return (
<Wrapper>
<ChannelInfoEmpty>
<ChannelLogoEmpty
style={{
backgroundImage: channel.icon ? `url(${channel.icon}` : "",
}}
>
<ChannelLogoEmpty icon={channel.icon}>
{" "}
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
</ChannelLogoEmpty>
<ChannelNameEmpty className={"active"}>
{channel.name.slice(0, 10)}
</ChannelNameEmpty>
<ChannelNameEmpty active={true} channel={channel} />
</ChannelInfoEmpty>
{channel.type === "dm" ? (

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import styled from "styled-components";
import { ChatState, useChatState } from "../contexts/chatStateProvider";
import { useNarrow } from "../contexts/narrowProvider";
import { Channels } from "./Channels/Channels";
@ -23,9 +24,8 @@ function Modals() {
}
export function Chat() {
const [showMembers, setShowMembers] = useState(true);
const [createChat, setCreateChat] = useState(false);
const [state] = useChatState();
const [showMembers, setShowMembers] = useState(false);
const narrow = useNarrow();
return (
@ -33,18 +33,17 @@ export function Chat() {
{!narrow && (
<ChannelsWrapper>
<StyledCommunity />
<Channels setCreateChat={setCreateChat} />
<Channels />
</ChannelsWrapper>
)}
{!createChat && (
{state === ChatState.ChatBody && (
<ChatBody
onClick={() => setShowMembers(!showMembers)}
showMembers={showMembers}
setCreateChat={setCreateChat}
/>
)}
{showMembers && !narrow && !createChat && <Members />}
{createChat && <ChatCreation setCreateChat={setCreateChat} />}
{showMembers && !narrow && state === ChatState.ChatBody && <Members />}
{state === ChatState.ChatCreation && <ChatCreation />}
<Modals />
</ChatWrapper>
);

View File

@ -27,14 +27,9 @@ enum ChatBodyState {
interface ChatBodyProps {
onClick: () => void;
showMembers: boolean;
setCreateChat: (val: boolean) => void;
}
export function ChatBody({
onClick,
showMembers,
setCreateChat,
}: ChatBodyProps) {
export function ChatBody({ onClick, showMembers }: ChatBodyProps) {
const { messenger, activeChannel, communityData } = useMessengerContext();
const narrow = useNarrow();
const [showChannelMenu, setShowChannelMenu] = useState(false);
@ -61,7 +56,7 @@ export function ChatBody({
return (
<ChatBodyWrapper className={className}>
{editGroup && communityData ? (
<ChatCreation setCreateChat={setCreateChat} editGroup={editGroup} />
<ChatCreation editGroup={editGroup} />
) : (
<ChatTopbar
className={narrow && showState !== ChatBodyState.Chat ? "narrow" : ""}
@ -78,10 +73,9 @@ export function ChatBody({
<Channel
channel={activeChannel}
isActive={
narrow ? showState === ChatBodyState.Channels : true
narrow ? showState === ChatBodyState.Channels : false
}
activeView={true}
isMuted={false}
onClick={() => switchShowState(ChatBodyState.Channels)}
/>
</>
@ -132,7 +126,6 @@ export function ChatBody({
{showState === ChatBodyState.Channels && (
<NarrowChannels
setShowChannels={() => switchShowState(ChatBodyState.Channels)}
setCreateChat={setCreateChat}
/>
)}
{showState === ChatBodyState.Members && (

View File

@ -2,6 +2,7 @@ import React, { useCallback, useState } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components";
import { ChatState, useChatState } from "../../contexts/chatStateProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { buttonStyles } from "../Buttons/buttonStyle";
@ -10,16 +11,16 @@ import { Member } from "../Members/Member";
import { SearchBlock } from "../SearchBlock";
import { textMediumStyles } from "../Text";
interface ChatCreationProps {
setCreateChat: (val: boolean) => void;
editGroup?: boolean;
}
export function ChatCreation({ setCreateChat, editGroup }: ChatCreationProps) {
export function ChatCreation({ editGroup }: ChatCreationProps) {
const identity = useIdentity();
const [query, setQuery] = useState("");
const [styledGroup, setStyledGroup] = useState<string[]>([]);
const { contacts, setChannel } = useMessengerContext();
const setChatState = useChatState()[1];
const addMember = useCallback(
(member: string) => {
@ -51,7 +52,7 @@ export function ChatCreation({ setCreateChat, editGroup }: ChatCreationProps) {
type: "dm",
description: "Contact",
});
setCreateChat(false);
setChatState(ChatState.ChatBody);
};
return (

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Identity } from "status-communities/dist/cjs";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { IdentityProvider } from "../contexts/identityProvider";
import { MessengerProvider } from "../contexts/messengerProvider";
@ -18,7 +19,9 @@ export function ChatLoader({ communityKey }: ChatLoaderProps) {
return (
<IdentityProvider identity={identity}>
<MessengerProvider identity={identity} communityKey={communityKey}>
<ChatStateProvider>
<Chat />
</ChatStateProvider>
</MessengerProvider>
</IdentityProvider>
);

View File

@ -2,7 +2,6 @@ import React, { useMemo } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components";
import { useFriends } from "../../contexts/friendsProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
@ -33,10 +32,8 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
);
const { setModal } = useModal(ProfileModalName);
const { friends, setFriends } = useFriends();
const userIsFriend = useMemo(() => friends.includes(id), [friends, id]);
const { contact, setBlocked, setIsUntrustworthy } = useManageContact(id);
const { contact, setBlocked, setIsUntrustworthy, setIsUserFriend } =
useManageContact(id);
if (!contact) return null;
return (
@ -63,13 +60,13 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
<ProfileSvg width={16} height={16} />
<MenuText>View Profile</MenuText>
</MenuItem>
{!userIsFriend && (
<MenuItem onClick={() => setFriends((prev) => [...prev, id])}>
{!contact.isFriend && (
<MenuItem onClick={() => setIsUserFriend(true)}>
<AddContactSvg width={16} height={16} />
<MenuText>Send Contact Request</MenuText>
</MenuItem>
)}
{userIsFriend && (
{contact.isFriend && (
<MenuItem>
<ChatSvg width={16} height={16} />
<MenuText>Send Message</MenuText>
@ -98,7 +95,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
</MenuText>
</MenuItem>
{!userIsFriend && !isUser && (
{!contact.isFriend && !isUser && (
<MenuItem
onClick={() => {
setBlocked(!contact.blocked);

View File

@ -55,15 +55,7 @@ export const EditModal = () => {
</NameSection>
<LogoSection>
<Label>Group image</Label>
<GroupLogo
style={{
backgroundImage: image
? `url(${image}`
: activeChannel.icon
? `url(${activeChannel.icon}`
: "",
}}
>
<GroupLogo icon={image || activeChannel.icon}>
{!activeChannel.icon &&
!image &&
activeChannel.name.slice(0, 1).toUpperCase()}

View File

@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useState } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components";
import { useFriends } from "../../contexts/friendsProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
@ -40,17 +39,18 @@ export const ProfileModal = () => {
[id, identity]
);
const { friends, setFriends } = useFriends();
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 {
contact,
setBlocked,
setCustomName,
setIsUntrustworthy,
setIsUserFriend,
} = useManageContact(id);
const [customNameInput, setCustomNameInput] = useState("");
if (!contact) return null;
@ -134,7 +134,7 @@ export const ProfileModal = () => {
</>
) : (
<>
{!userIsFriend && !isUser && (
{!contact.isFriend && !isUser && (
<ProfileBtn
className={contact.blocked ? "" : "red"}
onClick={() => {
@ -144,12 +144,10 @@ export const ProfileModal = () => {
{contact.blocked ? "Unblock" : "Block"}
</ProfileBtn>
)}
{userIsFriend && (
{contact.isFriend && (
<ProfileBtn
className="red"
onClick={() =>
setFriends((prev) => prev.filter((e) => e != id))
}
onClick={() => setIsUserFriend(false)}
>
Remove Contact
</ProfileBtn>
@ -162,8 +160,8 @@ export const ProfileModal = () => {
? "Remove Untrustworthy Mark"
: "Mark as Untrustworthy"}
</ProfileBtn>
{!userIsFriend && (
<Btn onClick={() => setFriends((prev) => [...prev, id])}>
{!contact.isFriend && (
<Btn onClick={() => setIsUserFriend(true)}>
Send Contact Request
</Btn>
)}

View File

@ -7,20 +7,13 @@ import { NarrowTopbar } from "./NarrowTopbar";
interface NarrowChannelsProps {
setShowChannels: (val: boolean) => void;
setCreateChat: (val: boolean) => void;
}
export function NarrowChannels({
setShowChannels,
setCreateChat,
}: NarrowChannelsProps) {
export function NarrowChannels({ setShowChannels }: NarrowChannelsProps) {
return (
<ListWrapper>
<NarrowTopbar list="Channels" />
<Channels
onCommunityClick={() => setShowChannels(false)}
setCreateChat={setCreateChat}
/>
<Channels onCommunityClick={() => setShowChannels(false)} />
</ListWrapper>
);
}

View File

@ -3,7 +3,6 @@ import { ThemeProvider } from "styled-components";
import styled from "styled-components";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { FriendsProvider } from "../contexts/friendsProvider";
import { ModalProvider } from "../contexts/modalProvider";
import { NarrowProvider } from "../contexts/narrowProvider";
import { Metadata } from "../models/Metadata";
@ -28,7 +27,6 @@ export function ReactChat({
<ThemeProvider theme={theme}>
<NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<FriendsProvider>
<ModalProvider>
<Wrapper ref={ref}>
<GlobalStyle />
@ -36,7 +34,6 @@ export function ReactChat({
<div id="modal-root" />
</Wrapper>
</ModalProvider>
</FriendsProvider>
</FetchMetadataProvider>
</NarrowProvider>
</ThemeProvider>

View File

@ -3,8 +3,8 @@ import styled from "styled-components";
import { useMessengerContext } from "../contexts/messengerProvider";
import { Channel } from "./Channels/Channel";
import { ContactsList } from "./Chat/ChatCreation";
import { Member } from "./Members/Member";
interface SearchBlockProps {
query: string;
@ -39,15 +39,9 @@ export const SearchBlock = ({
.filter((member) => member.id.includes(query))
.filter((member) => !dsicludeList.includes(member.id))
.map((member) => (
<Channel
<Member
key={member.id}
channel={{
id: member.id,
name: member.id.slice(0, 10),
type: "dm",
}}
isActive={false}
isMuted={false}
contact={member}
onClick={() => onClick(member.id)}
/>
))}

View File

@ -0,0 +1,25 @@
import React, { createContext, useContext, useState } from "react";
export enum ChatState {
ChatCreation,
ChatBody,
}
type ChatStateContextType = [
ChatState,
React.Dispatch<React.SetStateAction<ChatState>>
];
const ChatStateContext = createContext<ChatStateContextType>([
ChatState.ChatBody,
() => undefined,
]);
export function useChatState() {
return useContext(ChatStateContext);
}
export function ChatStateProvider({ children }: { children: React.ReactNode }) {
const state = useState(ChatState.ChatBody);
return <ChatStateContext.Provider value={state} children={children} />;
}

View File

@ -1,27 +0,0 @@
import React, { createContext, useContext, useState } from "react";
const FriendsContext = createContext<{
friends: string[];
setFriends: React.Dispatch<React.SetStateAction<string[]>>;
}>({
friends: [],
setFriends: () => undefined,
});
export function useFriends() {
return useContext(FriendsContext);
}
interface FriendsProviderProps {
children: React.ReactNode;
}
export function FriendsProvider({ children }: FriendsProviderProps) {
const [friends, setFriends] = useState<string[]>([]);
return (
<FriendsContext.Provider
value={{ friends, setFriends }}
children={children}
/>
);
}

View File

@ -19,6 +19,7 @@ const MessengerContext = createContext<MessengerType>({
activeChannel: {
id: "",
name: "",
type: "channel",
},
channels: {},
setChannel: () => undefined,

View File

@ -49,6 +49,7 @@ export function useMessenger(
id: "",
name: "",
description: "",
type: "channel",
});
const chatId = useMemo(() => activeChannel.id, [activeChannel]);

View File

@ -38,5 +38,23 @@ export function useManageContact(id: string) {
},
[id]
);
return { contact, setCustomName, setBlocked, setIsUntrustworthy };
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

@ -3,7 +3,7 @@ import { Contact } from "./Contact";
export type ChannelData = {
id: string;
name: string;
type?: "channel" | "dm" | "group";
type: "channel" | "dm" | "group";
description?: string;
icon?: string;
isMuted?: boolean;

View File

@ -5,6 +5,7 @@ export type Contact = {
customName?: string;
isUntrustworthy: boolean;
blocked: boolean;
isFriend?: boolean;
};
export type Contacts = {

View File

@ -14,7 +14,6 @@ export const GlobalStyle = createGlobalStyle`
html {
margin: 0;
font-family: 'Inter', sans-serif;
font-weight: normal;
font-size: 15px;
line-height: 22px;
}