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 27516f66..114df1bf 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 6d984f19..e2c653f3 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 b424b8e5..041e2031 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 c5b9513d..e4d1e923 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 00000000..f7e5dfea
--- /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 372ace1f..4d7d8d51 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 00000000..78744b87
--- /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 00000000..e54312c2
--- /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 47ab363a..ce9f93ba 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 00000000..749b0f3e
--- /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 83d3dad9..3064315d 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;