Add group chat (#98)

This commit is contained in:
Maria Rushkova 2021-10-29 09:41:36 +02:00 committed by GitHub
parent 29d4e76f88
commit 892e06fdb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 510 additions and 54 deletions

View File

@ -3,6 +3,7 @@ import styled from "styled-components";
import { useNarrow } from "../../contexts/narrowProvider";
import { ChannelData } from "../../models/ChannelData";
import { GroupIcon } from "../Icons/GroupIcon";
import { MutedIcon } from "../Icons/MutedIcon";
import { textMediumStyles } from "../Text";
@ -58,7 +59,14 @@ export function Channel({
: ""
}
>
# {channel.name}
{channel.type && channel.type === "group" ? (
<GroupIcon />
) : channel.type === "dm" ? (
""
) : (
"#"
)}{" "}
{channel.name}
</ChannelName>
{activeView && (
<ChannelDescription> {channel.description}</ChannelDescription>
@ -131,10 +139,14 @@ export const ChannelLogo = styled.div`
}
`;
export const ChannelName = styled.p`
export const ChannelName = styled.div`
font-weight: 500;
opacity: 0.7;
color: ${({ theme }) => theme.primary};
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
${textMediumStyles}
&.active,

View File

@ -2,6 +2,7 @@ import React, { useEffect } from "react";
import styled from "styled-components";
import { ChannelData } from "../../models/ChannelData";
import { EditIcon } from "../Icons/EditIcon";
import { Channel } from "./Channel";
@ -12,6 +13,7 @@ interface ChannelsProps {
activeChannelId: string;
channels: ChannelData[];
membersList: string[];
setCreateChat: (val: boolean) => void;
}
export function Channels({
@ -21,6 +23,7 @@ export function Channels({
activeChannelId,
channels,
membersList,
setCreateChat,
}: ChannelsProps) {
useEffect(() => {
const channel = channels.find((channel) => channel.id === activeChannelId);
@ -46,18 +49,27 @@ export function Channels({
}
onClick={() => {
onCommunityClick(channel);
setCreateChat(false);
}}
/>
))}
{membersList.length > 0 && (
<Dialogues>
{membersList.map((member) => (
<Chats>
<ChatsBar>
<Heading>Chat</Heading>
<EditBtn onClick={() => setCreateChat(true)}>
<EditIcon />
</EditBtn>
</ChatsBar>
<ChatsList>
{membersList.length > 0 &&
membersList.map((member) => (
<Channel
key={member}
channel={{
id: member,
name: member.slice(0, 10),
name: member,
type: "group",
description: "Contact",
}}
isActive={member === activeChannelId}
@ -68,11 +80,12 @@ export function Channels({
name: member.slice(0, 10),
description: "Contact",
});
setCreateChat(false);
}}
/>
))}
</Dialogues>
)}
</ChatsList>
</Chats>
</ChannelList>
);
}
@ -87,7 +100,7 @@ export const ChannelList = styled.div`
}
`;
const Dialogues = styled.div`
const Chats = styled.div`
display: flex;
flex-direction: column;
padding-top: 16px;
@ -106,3 +119,39 @@ const Dialogues = styled.div`
opacity: 0.1;
}
`;
const ChatsBar = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
`;
const ChatsList = styled.div`
display: flex;
flex-direction: column;
overflow: auto;
`;
const Heading = styled.p`
font-weight: bold;
font-size: 17px;
line-height: 24px;
color: ${({ theme }) => theme.primary};
`;
const EditBtn = styled.button`
width: 32px;
height: 32px;
border-radius: 8px;
padding: 0;
&:hover {
background: ${({ theme }) => theme.border};
}
&:active,
&.active {
background: ${({ theme }) => theme.inputColor};
}
`;

View File

@ -25,11 +25,16 @@ export function EmptyChannel({ channel }: EmptyChannelProps) {
<ChannelNameEmpty className={"active"}>{channel.name}</ChannelNameEmpty>
</ChannelInfoEmpty>
{channel.description === "Contact" ? (
{channel.type === "dm" ? (
<EmptyText>
Any messages you send here are encrypted and can only be read by you
and <span>{channel.name}</span>.
</EmptyText>
) : channel.type === "group" ? (
<EmptyText>
You created a group with <span>{channel.name.slice(1, -1)}</span> and{" "}
<span>{channel.name.at(-1)}</span>
</EmptyText>
) : (
<EmptyText>
Welcome to the beginning of the <span>#{channel.name}</span> channel!

View File

@ -11,6 +11,7 @@ import { uintToImgUrl } from "../utils/uintToImgUrl";
import { Channels } from "./Channels/Channels";
import { ChatBody } from "./Chat/ChatBody";
import { ChatCreation } from "./Chat/ChatCreation";
import { Community } from "./Community";
import { Members } from "./Members/Members";
import { CommunityModal } from "./Modals/CommunityModal";
@ -37,6 +38,7 @@ export function Chat({
const [showMembers, setShowMembers] = useState(true);
const [showChannels, setShowChannels] = useState(true);
const [membersList, setMembersList] = useState([]);
const [createChat, setCreateChat] = useState(false);
const narrow = useNarrow();
@ -107,9 +109,12 @@ export function Chat({
activeChannelId={activeChannel?.id ?? ""}
channels={channels}
membersList={membersList}
setCreateChat={setCreateChat}
/>
</ChannelsWrapper>
)}
{!createChat && (
<ChatBody
identity={identity}
contacts={contacts}
@ -133,8 +138,10 @@ export function Chat({
channels={channels}
membersList={membersList}
setMembersList={setMembersList}
setCreateChat={setCreateChat}
/>
{showMembers && !narrow && (
)}
{showMembers && !narrow && !createChat && (
<Members
identity={identity}
contacts={contacts}
@ -142,6 +149,14 @@ export function Chat({
setMembersList={setMembersList}
/>
)}
{createChat && communityData && (
<ChatCreation
community={communityData}
setMembersList={setMembersList}
setActiveChannel={setActiveChannel}
setCreateChat={setCreateChat}
/>
)}
{communityData && (
<CommunityModal
isVisible={isModalVisible}

View File

@ -45,6 +45,7 @@ interface ChatBodyProps {
channels: ChannelData[];
membersList: string[];
setMembersList: any;
setCreateChat: (val: boolean) => void;
}
export function ChatBody({
@ -70,6 +71,7 @@ export function ChatBody({
channels,
membersList,
setMembersList,
setCreateChat,
}: ChatBodyProps) {
const narrow = useNarrow();
const [showChannelsList, setShowChannelsList] = useState(false);
@ -165,6 +167,7 @@ export function ChatBody({
activeChannelId={activeChannelId}
clearNotifications={clearNotifications}
membersList={membersList}
setCreateChat={setCreateChat}
/>
)}
{showMembersList && narrow && (
@ -251,6 +254,11 @@ const MemberBtn = styled.button`
padding: 0;
margin-top: 12px;
&:hover {
background: ${({ theme }) => theme.border};
}
&:active,
&.active {
background: ${({ theme }) => theme.inputColor};
}

View File

@ -0,0 +1,227 @@
import React, { useState } from "react";
import styled from "styled-components";
import { ChannelData } from "../../models/ChannelData";
import { CommunityData } from "../../models/CommunityData";
import { buttonStyles } from "../Buttons/buttonStyle";
import { Channel } from "../Channels/Channel";
import { CrossIcon } from "../Icons/CrossIcon";
import { SearchBlock } from "../SearchBlock";
import { textMediumStyles } from "../Text";
interface ChatCreationProps {
community: CommunityData;
setMembersList: any;
setActiveChannel: (val: ChannelData) => void;
setCreateChat: (val: boolean) => void;
}
export function ChatCreation({
community,
setMembersList,
setActiveChannel,
setCreateChat,
}: ChatCreationProps) {
const [query, setQuery] = useState("");
const [styledGroup, setStyledGroup] = useState<string[]>([]);
const addMember = (member: string) => {
setStyledGroup((prevMembers: string[]) => {
if (prevMembers.find((mem) => mem === member)) {
return prevMembers;
} else {
return [...prevMembers, member];
}
});
};
const removeMember = (member: string) => {
const idx = styledGroup.indexOf(member);
styledGroup.splice(idx, 1);
};
const createChat = (group: string[]) => {
setMembersList(group);
setActiveChannel({
id: group.join(""),
name: group.join(", "),
type: "dm",
});
setCreateChat(false);
};
return (
<CreationWrapper>
<CreationBar>
<InputBar>
<InputText>To:</InputText>
{styledGroup.length > 0 && (
<StyledList>
{styledGroup.map((member) => (
<StyledMember key={member}>
<StyledName>{member.slice(0, 10)}</StyledName>
<CloseButton onClick={() => removeMember(member)}>
<CrossIcon memberView={true} />
</CloseButton>
</StyledMember>
))}
</StyledList>
)}
<SearchMembers>
{styledGroup.length < 5 && (
<>
<Input
value={query}
onInput={(e) => setQuery(e.currentTarget.value)}
/>
{query && (
<SearchBlock
community={community}
query={query}
styledGroup={styledGroup}
setStyledGroup={setStyledGroup}
/>
)}
</>
)}
</SearchMembers>
{styledGroup.length === 5 && (
<LimitAlert>5 user Limit reached</LimitAlert>
)}
</InputBar>
<CreationBtn
disabled={styledGroup.length === 0}
onClick={() => createChat(styledGroup)}
>
Confirm
</CreationBtn>
</CreationBar>
{!query && styledGroup.length === 0 && (
<Contacts>
<ContactsHeading>Contacts</ContactsHeading>
<ContactsList>
{community.membersList.map((member) => (
<Channel
key={member}
channel={{
id: member,
name: member.slice(0, 10),
type: "dm",
}}
isActive={false}
isMuted={false}
onClick={() => addMember(member)}
/>
))}
</ContactsList>
</Contacts>
)}
</CreationWrapper>
);
}
const CreationWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
background-color: ${({ theme }) => theme.bodyBackgroundColor};
padding: 8px 16px;
`;
const CreationBar = styled.div`
display: flex;
margin-bottom: 32px;
`;
const InputBar = styled.div`
display: flex;
align-items: center;
flex: 1;
width: 100%;
background-color: ${({ theme }) => theme.inputColor};
border: 1px solid ${({ theme }) => theme.inputColor};
border-radius: 8px;
padding: 8px 16px;
${textMediumStyles}
`;
const Input = styled.input`
background-color: ${({ theme }) => theme.inputColor};
border: 1px solid ${({ theme }) => theme.inputColor};
outline: none;
resize: none;
${textMediumStyles}
&:focus {
outline: none;
caret-color: ${({ theme }) => theme.notificationColor};
}
`;
const InputText = styled.div`
color: ${({ theme }) => theme.secondary};
margin-right: 8px;
`;
const CreationBtn = styled.button`
padding: 11px 24px;
margin-left: 16px;
${buttonStyles}
&:disabled {
background: ${({ theme }) => theme.inputColor};
color: ${({ theme }) => theme.secondary};
}
`;
const StyledList = styled.div`
display: flex;
`;
const StyledMember = styled.div`
display: flex;
align-items: center;
padding: 4px 4px 4px 8px;
background: ${({ theme }) => theme.tertiary};
color: ${({ theme }) => theme.bodyBackgroundColor};
border-radius: 8px;
margin-right: 8px;
`;
const StyledName = styled.p`
color: ${({ theme }) => theme.bodyBackgroundColor};
${textMediumStyles}
`;
const CloseButton = styled.button`
width: 20px;
height: 20px;
`;
const Contacts = styled.div`
display: flex;
flex-direction: column;
`;
const ContactsHeading = styled.p`
color: ${({ theme }) => theme.secondary};
${textMediumStyles}
`;
export const ContactsList = styled.div`
display: flex;
flex-direction: column;
`;
const SearchMembers = styled.div`
position: relative;
`;
const LimitAlert = styled.p`
text-transform: uppercase;
margin-left: auto;
color: ${({ theme }) => theme.redColor};
`;

View File

@ -1,19 +1,23 @@
import React from "react";
import styled from "styled-components";
export const CrossIcon = () => (
interface CrossIconProps {
memberView?: boolean;
}
export const CrossIcon = ({ memberView }: CrossIconProps) => (
<Icon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={memberView ? "white" : ""}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 4.57404L1.72275 0.296796C1.32616 -0.0997918 0.689941 -0.0975927 0.296174 0.296174C-0.100338 0.692686 -0.0973145 1.32864 0.296796 1.72275L4.57404 6L0.296796 10.2772C-0.0997918 10.6738 -0.0975927 11.3101 0.296174 11.7038C0.692686 12.1003 1.32864 12.0973 1.72275 11.7032L6 7.42596L10.2772 11.7032C10.6738 12.0998 11.3101 12.0976 11.7038 11.7038C12.1003 11.3073 12.0973 10.6714 11.7032 10.2772L7.42596 6L11.7032 1.72275C12.0998 1.32616 12.0976 0.689941 11.7038 0.296174C11.3073 -0.100338 10.6714 -0.0973145 10.2772 0.296796L6 4.57404Z"
fill="black"
/>
</Icon>
);
@ -22,4 +26,10 @@ const Icon = styled.svg`
& > path {
fill: ${({ theme }) => theme.primary};
}
&.white {
& > path {
fill: ${({ theme }) => theme.bodyBackgroundColor};
}
}
`;

View File

@ -0,0 +1,27 @@
import React from "react";
import styled from "styled-components";
export const EditIcon = () => {
return (
<Icon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.25 3.50019C11.6642 3.50019 12 3.1644 12 2.75019C12 2.33598 11.6642 2.00019 11.25 2.00019L6 2.00019C3.79086 2.00019 2 3.79105 2 6.00019L2 18.0002C2 20.2093 3.79086 22.0002 6 22.0002H18C20.2091 22.0002 22 20.2093 22 18.0002V12.7502C22 12.336 21.6642 12.0002 21.25 12.0002C20.8358 12.0002 20.5 12.336 20.5 12.7502V18.0002C20.5 19.3809 19.3807 20.5002 18 20.5002H6C4.61929 20.5002 3.5 19.3809 3.5 18.0002L3.5 6.00019C3.5 4.61948 4.61929 3.50019 6 3.50019L11.25 3.50019Z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.2803 2.71986C20.2971 1.73661 18.7029 1.73661 17.7197 2.71986L9.33613 11.1034C9.00567 11.4339 8.76487 11.8431 8.63648 12.2925L7.77886 15.2941C7.70403 15.556 7.77707 15.8379 7.96967 16.0305C8.16227 16.2231 8.44415 16.2962 8.70604 16.2213L11.7077 15.3637C12.1571 15.2353 12.5663 14.9945 12.8968 14.6641L21.2803 6.28052C22.2636 5.29727 22.2636 3.70311 21.2803 2.71986ZM18.7803 3.78052C19.1778 3.38306 19.8222 3.38306 20.2197 3.78052C20.6171 4.17798 20.6171 4.8224 20.2197 5.21986L11.8361 13.6034C11.6859 13.7536 11.4999 13.8631 11.2956 13.9214C10.5531 14.1336 9.86662 13.4471 10.0788 12.7045C10.1371 12.5003 10.2466 12.3143 10.3968 12.1641L18.7803 3.78052Z"
/>
</Icon>
);
};
const Icon = styled.svg`
& > path {
fill: ${({ theme }) => theme.primary};
}
`;

View File

@ -0,0 +1,25 @@
import React from "react";
import styled from "styled-components";
export const GroupIcon = () => {
return (
<Icon
width="14"
height="10"
viewBox="0 0 14 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7 4.5C8.24265 4.5 9.25 3.49264 9.25 2.25C9.25 1.00736 8.24265 0 7 0C5.75736 0 4.75 1.00736 4.75 2.25C4.75 3.49264 5.75736 4.5 7 4.5Z" />
<path d="M3.12343 9.01012C3.56395 7.27976 5.13252 6 7 6C8.86749 6 10.4361 7.27976 10.8766 9.01012C11.0128 9.54533 10.5523 10 10 10H4C3.44772 10 2.98718 9.54533 3.12343 9.01012Z" />
<path d="M3.5 4.25C3.5 5.2165 2.7165 6 1.75 6C0.783502 6 0 5.2165 0 4.25C0 3.2835 0.783502 2.5 1.75 2.5C2.7165 2.5 3.5 3.2835 3.5 4.25Z" />
<path d="M12.25 6C13.2165 6 14 5.2165 14 4.25C14 3.2835 13.2165 2.5 12.25 2.5C11.2835 2.5 10.5 3.2835 10.5 4.25C10.5 5.2165 11.2835 6 12.25 6Z" />
</Icon>
);
};
const Icon = styled.svg`
& > path {
fill: ${({ theme }) => theme.secondary};
}
`;

View File

@ -14,8 +14,8 @@ interface NarrowChannelsProps {
setShowChannels: (val: boolean) => void;
clearNotifications: (id: string) => void;
channels: ChannelData[];
membersList: string[];
setCreateChat: (val: boolean) => void;
}
export function NarrowChannels({
@ -26,8 +26,8 @@ export function NarrowChannels({
setShowChannels,
clearNotifications,
channels,
membersList,
setCreateChat,
}: NarrowChannelsProps) {
return (
<ListWrapper>
@ -42,6 +42,7 @@ export function NarrowChannels({
activeChannelId={activeChannelId}
channels={channels}
membersList={membersList}
setCreateChat={setCreateChat}
/>
</ListWrapper>
);

View File

@ -0,0 +1,76 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import { CommunityData } from "../models/CommunityData";
import { Channel } from "./Channels/Channel";
import { ContactsList } from "./Chat/ChatCreation";
interface SearchBlockProps {
community: CommunityData;
query: string;
styledGroup: string[];
setStyledGroup: React.Dispatch<React.SetStateAction<string[]>>;
}
export const SearchBlock = ({
community,
query,
styledGroup,
setStyledGroup,
}: SearchBlockProps) => {
const searchList = useMemo(() => {
return community.membersList
.filter((member) => member.includes(query))
.filter((member) => !styledGroup.includes(member));
}, [query, styledGroup, community.membersList]);
const addMember = (member: string) => {
setStyledGroup((prevMembers: string[]) => {
if (prevMembers.find((mem) => mem === member)) {
return prevMembers;
} else {
return [...prevMembers, member];
}
});
};
if (searchList.length === 0) {
return null;
}
return (
<SearchContacts>
<ContactsList>
{community.membersList
.filter((member) => member.includes(query))
.filter((member) => !styledGroup.includes(member))
.map((member) => (
<Channel
key={member}
channel={{
id: member,
name: member.slice(0, 10),
type: "dm",
}}
isActive={false}
isMuted={false}
onClick={() => addMember(member)}
/>
))}
</ContactsList>
</SearchContacts>
);
};
const SearchContacts = styled.div`
display: flex;
flex-direction: column;
width: 360px;
padding: 8px;
background-color: ${({ theme }) => theme.bodyBackgroundColor};
box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16),
0px 4px 12px rgba(0, 34, 51, 0.08);
border-radius: 8px;
position: absolute;
left: 0;
top: calc(100% + 24px);
`;

View File

@ -1,6 +1,7 @@
export type ChannelData = {
id: string;
name: string;
type?: "channel" | "dm" | "group";
description?: string;
icon?: string;
isMuted?: boolean;