From 892e06fdb108651b53d60ddf3f4ddf090a10d3a8 Mon Sep 17 00:00:00 2001 From: Maria Rushkova <66270386+mrushkova@users.noreply.github.com> Date: Fri, 29 Oct 2021 09:41:36 +0200 Subject: [PATCH] Add group chat (#98) --- .../src/components/Channels/Channel.tsx | 16 +- .../src/components/Channels/Channels.tsx | 93 +++++-- .../src/components/Channels/EmptyChannel.tsx | 7 +- packages/react-chat/src/components/Chat.tsx | 65 +++-- .../src/components/Chat/ChatBody.tsx | 8 + .../src/components/Chat/ChatCreation.tsx | 227 ++++++++++++++++++ .../src/components/Icons/CrossIcon.tsx | 14 +- .../src/components/Icons/EditIcon.tsx | 27 +++ .../src/components/Icons/GroupIcon.tsx | 25 ++ .../components/NarrowMode/NarrowChannels.tsx | 5 +- .../react-chat/src/components/SearchBlock.tsx | 76 ++++++ packages/react-chat/src/models/ChannelData.ts | 1 + 12 files changed, 510 insertions(+), 54 deletions(-) create mode 100644 packages/react-chat/src/components/Chat/ChatCreation.tsx create mode 100644 packages/react-chat/src/components/Icons/EditIcon.tsx create mode 100644 packages/react-chat/src/components/Icons/GroupIcon.tsx create mode 100644 packages/react-chat/src/components/SearchBlock.tsx diff --git a/packages/react-chat/src/components/Channels/Channel.tsx b/packages/react-chat/src/components/Channels/Channel.tsx index 27516f6..114df1b 100644 --- a/packages/react-chat/src/components/Channels/Channel.tsx +++ b/packages/react-chat/src/components/Channels/Channel.tsx @@ -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" ? ( + + ) : channel.type === "dm" ? ( + "" + ) : ( + "#" + )}{" "} + {channel.name} {activeView && ( {channel.description} @@ -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, diff --git a/packages/react-chat/src/components/Channels/Channels.tsx b/packages/react-chat/src/components/Channels/Channels.tsx index 6d984f1..e2c653f 100644 --- a/packages/react-chat/src/components/Channels/Channels.tsx +++ b/packages/react-chat/src/components/Channels/Channels.tsx @@ -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,33 +49,43 @@ export function Channels({ } onClick={() => { onCommunityClick(channel); + setCreateChat(false); }} /> ))} - {membersList.length > 0 && ( - - {membersList.map((member) => ( - { - onCommunityClick({ + + + Chat + setCreateChat(true)}> + + + + + {membersList.length > 0 && + membersList.map((member) => ( + - ))} - - )} + }} + isActive={member === activeChannelId} + isMuted={false} + onClick={() => { + onCommunityClick({ + id: member, + name: member.slice(0, 10), + description: "Contact", + }); + setCreateChat(false); + }} + /> + ))} + + ); } @@ -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}; + } +`; diff --git a/packages/react-chat/src/components/Channels/EmptyChannel.tsx b/packages/react-chat/src/components/Channels/EmptyChannel.tsx index b424b8e..041e203 100644 --- a/packages/react-chat/src/components/Channels/EmptyChannel.tsx +++ b/packages/react-chat/src/components/Channels/EmptyChannel.tsx @@ -25,11 +25,16 @@ export function EmptyChannel({ channel }: EmptyChannelProps) { {channel.name} - {channel.description === "Contact" ? ( + {channel.type === "dm" ? ( Any messages you send here are encrypted and can only be read by you and {channel.name}. + ) : channel.type === "group" ? ( + + You created a group with {channel.name.slice(1, -1)} and{" "} + {channel.name.at(-1)} + ) : ( Welcome to the beginning of the #{channel.name} channel! diff --git a/packages/react-chat/src/components/Chat.tsx b/packages/react-chat/src/components/Chat.tsx index c5b9513..e4d1e92 100644 --- a/packages/react-chat/src/components/Chat.tsx +++ b/packages/react-chat/src/components/Chat.tsx @@ -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,34 +109,39 @@ export function Chat({ activeChannelId={activeChannel?.id ?? ""} channels={channels} membersList={membersList} + setCreateChat={setCreateChat} /> )} - setShowMembers(!showMembers)} - showMembers={showMembers} - community={communityData} - showCommunity={!showChannels} - loadPrevDay={() => loadPrevDay(activeChannel.id)} - onCommunityClick={showModal} - fetchMetadata={fetchMetadata} - loadingMessages={loadingMessages} - clearNotifications={clearNotifications} - channels={channels} - membersList={membersList} - setMembersList={setMembersList} - /> - {showMembers && !narrow && ( + + {!createChat && ( + setShowMembers(!showMembers)} + showMembers={showMembers} + community={communityData} + showCommunity={!showChannels} + loadPrevDay={() => loadPrevDay(activeChannel.id)} + onCommunityClick={showModal} + fetchMetadata={fetchMetadata} + loadingMessages={loadingMessages} + clearNotifications={clearNotifications} + channels={channels} + membersList={membersList} + setMembersList={setMembersList} + setCreateChat={setCreateChat} + /> + )} + {showMembers && !narrow && !createChat && ( )} + {createChat && communityData && ( + + )} {communityData && ( 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}; } diff --git a/packages/react-chat/src/components/Chat/ChatCreation.tsx b/packages/react-chat/src/components/Chat/ChatCreation.tsx new file mode 100644 index 0000000..f7e5dfe --- /dev/null +++ b/packages/react-chat/src/components/Chat/ChatCreation.tsx @@ -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([]); + + 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 ( + + + + To: + {styledGroup.length > 0 && ( + + {styledGroup.map((member) => ( + + {member.slice(0, 10)} + removeMember(member)}> + + + + ))} + + )} + + {styledGroup.length < 5 && ( + <> + setQuery(e.currentTarget.value)} + /> + {query && ( + + )} + + )} + + {styledGroup.length === 5 && ( + 5 user Limit reached + )} + + createChat(styledGroup)} + > + Confirm + + + {!query && styledGroup.length === 0 && ( + + Contacts + + {community.membersList.map((member) => ( + addMember(member)} + /> + ))} + + + )} + + ); +} + +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}; +`; diff --git a/packages/react-chat/src/components/Icons/CrossIcon.tsx b/packages/react-chat/src/components/Icons/CrossIcon.tsx index 372ace1..4d7d8d5 100644 --- a/packages/react-chat/src/components/Icons/CrossIcon.tsx +++ b/packages/react-chat/src/components/Icons/CrossIcon.tsx @@ -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) => ( ); @@ -22,4 +26,10 @@ const Icon = styled.svg` & > path { fill: ${({ theme }) => theme.primary}; } + + &.white { + & > path { + fill: ${({ theme }) => theme.bodyBackgroundColor}; + } + } `; diff --git a/packages/react-chat/src/components/Icons/EditIcon.tsx b/packages/react-chat/src/components/Icons/EditIcon.tsx new file mode 100644 index 0000000..78744b8 --- /dev/null +++ b/packages/react-chat/src/components/Icons/EditIcon.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import styled from "styled-components"; + +export const EditIcon = () => { + return ( + + + + + ); +}; + +const Icon = styled.svg` + & > path { + fill: ${({ theme }) => theme.primary}; + } +`; diff --git a/packages/react-chat/src/components/Icons/GroupIcon.tsx b/packages/react-chat/src/components/Icons/GroupIcon.tsx new file mode 100644 index 0000000..e54312c --- /dev/null +++ b/packages/react-chat/src/components/Icons/GroupIcon.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import styled from "styled-components"; + +export const GroupIcon = () => { + return ( + + + + + + + ); +}; + +const Icon = styled.svg` + & > path { + fill: ${({ theme }) => theme.secondary}; + } +`; diff --git a/packages/react-chat/src/components/NarrowMode/NarrowChannels.tsx b/packages/react-chat/src/components/NarrowMode/NarrowChannels.tsx index 47ab363..ce9f93b 100644 --- a/packages/react-chat/src/components/NarrowMode/NarrowChannels.tsx +++ b/packages/react-chat/src/components/NarrowMode/NarrowChannels.tsx @@ -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 ( @@ -42,6 +42,7 @@ export function NarrowChannels({ activeChannelId={activeChannelId} channels={channels} membersList={membersList} + setCreateChat={setCreateChat} /> ); diff --git a/packages/react-chat/src/components/SearchBlock.tsx b/packages/react-chat/src/components/SearchBlock.tsx new file mode 100644 index 0000000..749b0f3 --- /dev/null +++ b/packages/react-chat/src/components/SearchBlock.tsx @@ -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>; +} + +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 ( + + + {community.membersList + .filter((member) => member.includes(query)) + .filter((member) => !styledGroup.includes(member)) + .map((member) => ( + addMember(member)} + /> + ))} + + + ); +}; + +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); +`; diff --git a/packages/react-chat/src/models/ChannelData.ts b/packages/react-chat/src/models/ChannelData.ts index 83d3dad..3064315 100644 --- a/packages/react-chat/src/models/ChannelData.ts +++ b/packages/react-chat/src/models/ChannelData.ts @@ -1,6 +1,7 @@ export type ChannelData = { id: string; name: string; + type?: "channel" | "dm" | "group"; description?: string; icon?: string; isMuted?: boolean;