Add activity center (#142)

This commit is contained in:
Maria Rushkova 2021-12-08 10:08:24 +01:00 committed by GitHub
parent 44da36dfdf
commit c7065341b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 809 additions and 82 deletions

View File

@ -0,0 +1,427 @@
import React, { useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useActivities } from "../contexts/activityProvider";
import { useMessengerContext } from "../contexts/messengerProvider";
import { useClickOutside } from "../hooks/useClickOutside";
import { Activity } from "../models/Activity";
import { equalDate } from "../utils/equalDate";
import { buttonStyles } from "./Buttons/buttonStyle";
import {
ContentWrapper,
DateSeparator,
MessageHeaderWrapper,
MessageOuterWrapper,
MessageText,
TimeWrapper,
UserAddress,
UserName,
UserNameWrapper,
} from "./Chat/ChatMessages";
import { ContactMenu } from "./Form/ContactMenu";
import { CheckSvg } from "./Icons/CheckIcon";
import { ClearSvg } from "./Icons/ClearIcon";
import { GroupIcon } from "./Icons/GroupIcon";
import { HideIcon } from "./Icons/HideIcon";
import { Icon } from "./Icons/Icon";
import { MoreIcon } from "./Icons/MoreIcon";
import { ReadIcon } from "./Icons/ReadIcon";
import { ReplyIcon } from "./Icons/ReplyActivityIcon";
import { ShowIcon } from "./Icons/ShowIcon";
import { UntrustworthIcon } from "./Icons/UntrustworthIcon";
import { UserIcon } from "./Icons/UserIcon";
import { textMediumStyles, textSmallStyles } from "./Text";
const today = new Date();
type ActivityMessageProps = {
activity: Activity;
};
function ActivityMessage({ activity }: ActivityMessageProps) {
const { contacts } = useMessengerContext();
const [showMenu, setShowMenu] = useState(false);
const type = activity.type;
const contact = useMemo(
() => contacts[activity.user],
[activity.user, contacts]
);
return (
<MessageOuterWrapper>
<ActivityDate>
{equalDate(activity.date, today)
? "Today"
: activity.date.toLocaleDateString()}
</ActivityDate>
<MessageWrapper className={`${!activity.isRead && "unread"}`}>
<>
<Icon>
<UserIcon />
</Icon>
<ActivityContent>
<MessageHeaderWrapper>
<UserNameWrapper>
<UserName>
{" "}
{contact.customName ?? activity.user.slice(0, 10)}
</UserName>
{contact.customName && (
<UserAddress>
{activity.user.slice(0, 5)}...{activity.user.slice(-3)}
</UserAddress>
)}
{contact.isUntrustworthy && <UntrustworthIcon />}
</UserNameWrapper>
<TimeWrapper>
{activity.date.toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</TimeWrapper>
</MessageHeaderWrapper>
{type === "request" && (
<ContextHeading>
Contact request
{activity.requestType === "outcome"
? ` to ${activity.user.slice(0, 10)}`
: ": "}
</ContextHeading>
)}
<ActivityText>
{activity.message?.content ||
(activity.requestType === "income" && activity.request)}
</ActivityText>
{type === "mention" &&
activity.channel &&
activity.channel.type !== "dm" && (
<Tag>
{activity.channel.type === "group" ? <GroupIcon /> : "#"}{" "}
<span>{` ${activity.channel.name.slice(0, 10)}`}</span>
</Tag>
)}
{type === "reply" && activity.quote && (
<ReplyWrapper>
{activity.quote.image && (
<ContextHeading>Posted an image in</ContextHeading>
)}
<Tag>
<ReplyIcon /> <span>{activity.quote.content}</span>
</Tag>
</ReplyWrapper>
)}
</ActivityContent>
</>
{type === "request" &&
!activity.status &&
activity.requestType === "income" && (
<>
<ActivityBtn
onClick={() => {
activity.isRead = true;
activity.status = "accepted";
}}
className="accept"
>
<CheckSvg width={20} height={20} className="accept" />
</ActivityBtn>
<ActivityBtn
onClick={() => {
activity.isRead = true;
activity.status = "declined";
}}
className="decline"
>
<ClearSvg width={20} height={20} className="decline" />
</ActivityBtn>
<ActivityBtn
onClick={() => {
setShowMenu((e) => !e);
}}
>
{showMenu && <ContactMenu id="1" setShowMenu={setShowMenu} />}
<MoreIcon />
</ActivityBtn>
</>
)}
{type === "request" && activity.status === "accepted" && (
<RequestStatus className="accepted">Accepted</RequestStatus>
)}
{type === "request" && activity.status === "declined" && (
<RequestStatus className="declined">Declined</RequestStatus>
)}
{type === "request" && activity.status === "sent" && (
<RequestStatus>Sent</RequestStatus>
)}
{type !== "request" && (
<ActivityBtn
onClick={() => {
activity.isRead = true;
}}
className={`${activity.isRead && "read"}`}
>
<ReadIcon isRead={activity.isRead} />
</ActivityBtn>
)}
</MessageWrapper>
</MessageOuterWrapper>
);
}
interface ActivityCenterProps {
setShowActivityCenter: (val: boolean) => void;
}
export function ActivityCenter({ setShowActivityCenter }: ActivityCenterProps) {
const { activities } = useActivities();
const { contacts } = useMessengerContext();
const ref = useRef(null);
useClickOutside(ref, () => setShowActivityCenter(false));
const shownActivities = useMemo(
() =>
activities.filter(
(activity) => !contacts?.[activity.user]?.blocked ?? true
),
[contacts, activities, activities.length]
);
const [hideRead, setHideRead] = useState(false);
const [filter, setFilter] = useState("");
const filteredActivities = shownActivities.filter((activity) =>
filter
? activity.type === filter
: hideRead
? activity.isRead !== true
: activity
);
return (
<ActivityBlock ref={ref}>
<ActivityFilter>
<Filters>
<FilterBtn onClick={() => setFilter("")}>All</FilterBtn>
<FilterBtn onClick={() => setFilter("mention")}>Mentions</FilterBtn>
<FilterBtn onClick={() => setFilter("reply")}>Replies</FilterBtn>
<FilterBtn onClick={() => setFilter("request")}>
Contact requests
</FilterBtn>
</Filters>
<Btns>
<ActivityBtn
onClick={() => {
shownActivities.map((activity) => (activity.isRead = true));
}}
>
<ReadIcon />
</ActivityBtn>
<ActivityBtn onClick={() => setHideRead(!hideRead)}>
{hideRead ? <ShowIcon /> : <HideIcon />}
</ActivityBtn>
</Btns>
</ActivityFilter>
{filteredActivities.length > 0 ? (
<Activities>
{filteredActivities.map((activity) => (
<ActivityMessage key={activity.id} activity={activity} />
))}
</Activities>
) : (
<EmptyActivities>Notifications will appear here</EmptyActivities>
)}
</ActivityBlock>
);
}
const ActivityBlock = styled.div`
width: 600px;
height: 770px;
display: flex;
flex-direction: column;
background: ${({ theme }) => theme.bodyBackgroundColor};
box-shadow: 0px 12px 24px rgba(0, 34, 51, 0.1);
border-radius: 8px;
position: absolute;
top: 48px;
right: 8px;
z-index: 100;
`;
const ActivityFilter = styled.div`
display: flex;
justify-content: space-between;
padding: 13px 16px;
`;
const Filters = styled.div`
display: flex;
`;
const FilterBtn = styled.button`
${buttonStyles}
${textSmallStyles}
padding: 10px 12px;
background: ${({ theme }) => theme.bodyBackgroundColor};
& + & {
margin-left: 8px;
}
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&:focus {
background: ${({ theme }) => theme.buttonBg};
}
`;
const ActivityBtn = styled.button`
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
align-self: center;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&.read {
&:hover {
background: ${({ theme }) => theme.bodyBackgroundColor};
}
}
&.accept {
&:hover {
background: rgba(78, 188, 96, 0.1);
}
}
&.decline {
&:hover {
background: rgba(255, 45, 85, 0.1);
}
}
& + & {
margin-left: 8px;
}
`;
const Activities = styled.div`
display: flex;
flex-direction: column;
width: 100%;
overflow: auto;
`;
const EmptyActivities = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex: 1;
width: 100%;
color: ${({ theme }) => theme.secondary};
`;
const ActivityDate = styled(DateSeparator)`
justify-content: flex-start;
padding: 8px 16px;
margin: 0;
`;
const MessageWrapper = styled.div`
width: 100%;
display: flex;
align-items: flex-start;
padding: 8px 16px;
&.unread {
background: ${({ theme }) => theme.buttonBgHover};
}
`;
const ActivityText = styled(MessageText)`
white-space: unset;
margin-bottom: 8px;
`;
const Tag = styled.div`
width: fit-content;
max-width: 200px;
display: flex;
align-items: center;
border: 1px solid ${({ theme }) => theme.secondary};
border-radius: 11px;
padding: 0 6px;
cursor: pointer;
font-weight: 500;
color: ${({ theme }) => theme.secondary};
${textSmallStyles}
& > span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
`;
const ContextHeading = styled.p`
font-style: italic;
color: ${({ theme }) => theme.secondary};
flex-shrink: 0;
${textMediumStyles}
`;
const RequestStatus = styled.p`
font-weight: 500;
align-self: center;
text-align: end;
color: ${({ theme }) => theme.secondary};
${textSmallStyles}
&.accepted {
color: ${({ theme }) => theme.greenColor};
}
&.declined {
color: ${({ theme }) => theme.redColor};
}
`;
const ActivityContent = styled(ContentWrapper)`
max-width: calc(100% - 80px);
flex: 1;
`;
const Btns = styled.div`
display: flex;
align-items: center;
`;
const ReplyWrapper = styled.div`
max-width: 100%;
display: flex;
align-items: center;
& > p {
margin-right: 4px;
}
`;

View File

@ -2,6 +2,7 @@ import { css } from "styled-components";
export const buttonStyles = css`
border-radius: 8px;
font-family: "Inter";
font-weight: 500;
font-size: 15px;
line-height: 22px;

View File

@ -188,6 +188,7 @@ const NotificationBagde = styled.div`
border-radius: 50%;
font-size: 12px;
line-height: 16px;
font-weight: 500;
background-color: ${({ theme }) => theme.notificationColor};
color: ${({ theme }) => theme.bodyBackgroundColor};
display: flex;

View File

@ -24,7 +24,10 @@ function Mention({ id, setMentioned }: MentionProps) {
const identity = useIdentity();
if (!contact) return <>{id}</>;
useEffect(() => {
if (contact.id === utils.bufToHex(identity.publicKey)) setMentioned(true);
}, [contact.id]);
return (
<MentionBLock onClick={() => setShowMenu(!showMenu)}>

View File

@ -1,14 +1,19 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { utils } from "status-communities/dist/cjs";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useChatScrollHandle } from "../../hooks/useChatScrollHandle";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils";
import { EmptyChannel } from "../Channels/EmptyChannel";
import { ContactMenu } from "../Form/ContactMenu";
import { Icon } from "../Icons/Icon";
import { LoadingIcon } from "../Icons/LoadingIcon";
import { QuoteSvg } from "../Icons/QuoteIcon";
import { ReactionSvg } from "../Icons/ReactionIcon";
@ -27,6 +32,7 @@ const today = new Date();
type ChatUiMessageProps = {
idx: number;
message: ChatMessage;
channel: ChannelData;
prevMessage: ChatMessage;
setImage: (img: string) => void;
setLink: (link: string) => void;
@ -36,6 +42,7 @@ type ChatUiMessageProps = {
function ChatUiMessage({
message,
channel,
idx,
prevMessage,
setImage,
@ -44,6 +51,9 @@ function ChatUiMessage({
quote,
}: ChatUiMessageProps) {
const { contacts } = useMessengerContext();
const { setActivities } = useActivities();
const identity = useIdentity();
const contact = useMemo(
() => contacts[message.sender],
[message.sender, contacts]
@ -51,6 +61,34 @@ function ChatUiMessage({
const [showMenu, setShowMenu] = useState(false);
const [mentioned, setMentioned] = useState(false);
useEffect(() => {
if (mentioned)
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "mention",
date: message.date,
user: message.sender,
message: message,
channel: channel,
},
]);
if (quote && quote.sender === utils.bufToHex(identity.publicKey))
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "reply",
date: message.date,
user: message.sender,
message: message,
channel: channel,
quote: quote,
},
]);
}, [mentioned, message, quote]);
return (
<MessageOuterWrapper>
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
@ -190,6 +228,7 @@ export function ChatMessages({ setReply }: ChatMessagesProps) {
<ChatUiMessage
key={message.id}
message={message}
channel={activeChannel}
idx={idx}
prevMessage={shownMessages[idx - 1]}
setLink={setLink}
@ -214,7 +253,7 @@ const MessagesWrapper = styled.div`
}
`;
const MessageWrapper = styled.div`
export const MessageWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
@ -242,7 +281,7 @@ const MessageWrapper = styled.div`
}
`;
const MessageOuterWrapper = styled.div`
export const MessageOuterWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
@ -254,7 +293,7 @@ const UserMessageWrapper = styled.div`
display: flex;
`;
const DateSeparator = styled.div`
export const DateSeparator = styled.div`
width: 100%;
display: flex;
flex: 1;
@ -272,38 +311,24 @@ const DateSeparator = styled.div`
${textSmallStyles}
`;
const ContentWrapper = styled.div`
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: 8px;
`;
const MessageHeaderWrapper = styled.div`
export const MessageHeaderWrapper = styled.div`
display: flex;
align-items: center;
`;
export const Icon = styled.div`
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: end;
border-radius: 50%;
background-color: #bcbdff;
background-size: contain;
background-position: center;
flex-shrink: 0;
position: relative;
cursor: pointer;
`;
const UserNameWrapper = styled.div`
export const UserNameWrapper = styled.div`
display: flex;
align-items: center;
`;
const UserName = styled.p`
export const UserName = styled.p`
font-weight: 500;
color: ${({ theme }) => theme.tertiary};
margin-right: 4px;
@ -331,7 +356,7 @@ export const UserAddress = styled.p`
}
`;
const TimeWrapper = styled.div`
export const TimeWrapper = styled.div`
font-size: 10px;
line-height: 14px;
letter-spacing: 0.2px;
@ -340,7 +365,7 @@ const TimeWrapper = styled.div`
margin-left: 4px;
`;
const MessageText = styled.div`
export const MessageText = styled.div`
overflow-wrap: anywhere;
width: 100%;
white-space: pre-wrap;

View File

@ -1,11 +1,14 @@
import React, { useState } from "react";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useNarrow } from "../../contexts/narrowProvider";
import { ActivityCenter } from "../ActivityCenter";
import { Channel } from "../Channels/Channel";
import { Community } from "../Community";
import { ChannelMenu } from "../Form/ChannelMenu";
import { ActivityIcon } from "../Icons/ActivityIcon";
import { MembersIcon } from "../Icons/MembersIcon";
import { MoreIcon } from "../Icons/MoreIcon";
import { CommunitySkeleton } from "../Skeleton/CommunitySkeleton";
@ -31,8 +34,10 @@ export function ChatTopbar({
setEditGroup,
}: ChatTopbarProps) {
const { messenger, activeChannel, communityData } = useMessengerContext();
const { activities } = useActivities();
const narrow = useNarrow();
const [showChannelMenu, setShowChannelMenu] = useState(false);
const [showActivityCenter, setShowActivityCenter] = useState(false);
return (
<Topbar
@ -63,13 +68,24 @@ export function ChatTopbar({
<MenuWrapper>
{!narrow && (
<MemberBtn onClick={onClick} className={showMembers ? "active" : ""}>
<TopBtn onClick={onClick} className={showMembers ? "active" : ""}>
<MembersIcon />
</MemberBtn>
</TopBtn>
)}
<MoreBtn onClick={() => setShowChannelMenu(!showChannelMenu)}>
<TopBtn onClick={() => setShowChannelMenu(!showChannelMenu)}>
<MoreIcon />
</MoreBtn>
</TopBtn>
<ActivityWrapper>
<TopBtn
onClick={() => setShowActivityCenter(!showActivityCenter)}
className="activity"
>
<ActivityIcon />
{activities.length > 0 && (
<NotificationBagde>{activities.length}</NotificationBagde>
)}
</TopBtn>
</ActivityWrapper>
</MenuWrapper>
{!messenger && !communityData && <Loading />}
{showChannelMenu && (
@ -80,10 +96,26 @@ export function ChatTopbar({
setEditGroup={setEditGroup}
/>
)}
{showActivityCenter && (
<ActivityCenter setShowActivityCenter={setShowActivityCenter} />
)}
</Topbar>
);
}
const Topbar = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&.narrow {
width: 100%;
}
`;
const ChannelWrapper = styled.div`
display: flex;
align-items: center;
@ -98,19 +130,6 @@ const SkeletonWrapper = styled.div`
padding: 8px;
`;
const Topbar = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&.narrow {
width: 100%;
}
`;
const CommunityWrap = styled.div`
padding-right: 10px;
margin-right: 16px;
@ -139,35 +158,61 @@ const MenuWrapper = styled.div`
align-items: center;
`;
const MemberBtn = styled.button`
const ActivityWrapper = styled.div`
padding-left: 10px;
margin-left: 10px;
position: relative;
&:before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 2px;
height: 24px;
transform: translateY(-50%);
border-radius: 1px;
background: ${({ theme }) => theme.primary};
opacity: 0.1;
}
`;
const TopBtn = styled.button`
width: 32px;
height: 32px;
border-radius: 8px;
padding: 0;
&:hover {
background: ${({ theme }) => theme.border};
background: ${({ theme }) => theme.sectionBackgroundColor};
}
&:active,
&.active {
background: ${({ theme }) => theme.inputColor};
}
`;
const MoreBtn = styled.button`
width: 32px;
height: 32px;
border-radius: 8px;
padding: 0;
margin: 0 8px;
&.activity {
&:hover {
background: ${({ theme }) => theme.border};
background: ${({ theme }) => theme.bodyBackgroundColor};
}
&:active,
&.active {
background: ${({ theme }) => theme.inputColor};
}
`;
const NotificationBagde = styled.div`
width: 18px;
height: 18px;
position: absolute;
top: -2px;
right: -2px;
border-radius: 50%;
font-size: 12px;
line-height: 16px;
font-weight: 500;
background-color: ${({ theme }) => theme.notificationColor};
color: ${({ theme }) => theme.bodyBackgroundColor};
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`;

View File

@ -0,0 +1,25 @@
import React from "react";
import styled from "styled-components";
export const ActivityIcon = () => {
return (
<Icon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.2815 19C14.9452 19 14.7036 19.3279 14.7349 19.6628C14.7449 19.7703 14.7504 19.8827 14.7504 20C14.7504 21.3889 13.5441 22.75 12.0004 22.75C10.4568 22.75 9.25041 21.3889 9.25041 20C9.25041 19.8827 9.25591 19.7703 9.26596 19.6628C9.29725 19.3279 9.05564 19 8.71934 19H5.52151C3.78523 19 2.87368 16.9394 4.04163 15.6547L5.21849 14.3601C5.72921 13.7983 6.06949 13.1028 6.19958 12.3548L7.31505 5.94085C7.71121 3.66293 9.6883 2 12.0004 2C14.3125 2 16.2896 3.66293 16.6858 5.94085L17.8012 12.3548C17.9313 13.1028 18.2716 13.7983 18.7823 14.3601L19.9592 15.6547C21.1271 16.9394 20.2156 19 18.4793 19H15.2815ZM11.0327 19.0283L11.0318 19.0293L11.0395 19.0214L11.0419 19.0188C11.0432 19.0175 11.0438 19.0168 11.0439 19.0167C11.0443 19.0163 11.0439 19.0167 11.0439 19.0167L11.0419 19.0188C11.0529 19.0073 11.0685 19 11.0845 19H12.9164C12.9323 19 12.9474 19.0068 12.9584 19.0183C12.9612 19.0217 12.9684 19.0302 12.9785 19.0438C13.0015 19.0744 13.0394 19.13 13.0796 19.2104C13.1587 19.3687 13.2504 19.6296 13.2504 20C13.2504 20.6111 12.6659 21.25 12.0004 21.25C11.3349 21.25 10.7504 20.6111 10.7504 20C10.7504 19.6296 10.8421 19.3687 10.9212 19.2104C10.9614 19.13 10.9993 19.0744 11.0223 19.0438C11.0325 19.0302 11.0391 19.0222 11.0419 19.0188L11.0395 19.0214L11.0377 19.0233L11.0345 19.0265L11.0327 19.0283ZM5.15154 16.6637L6.3284 15.3691C7.03064 14.5967 7.49853 13.6403 7.6774 12.6118L8.79286 6.19786C9.06407 4.63842 10.4176 3.5 12.0004 3.5C13.5833 3.5 14.9368 4.63842 15.208 6.19786L16.3234 12.6118C16.5023 13.6403 16.9702 14.5967 17.6724 15.3691L18.8493 16.6637C19.1413 16.9849 18.9134 17.5 18.4793 17.5H5.52151C5.08744 17.5 4.85956 16.9849 5.15154 16.6637Z"
fill="black"
/>
</Icon>
);
};
const Icon = styled.svg`
fill: ${({ theme }) => theme.primary};
`;

View File

@ -9,11 +9,10 @@ type CheckSvgProps = {
export function CheckSvg({ width, height, className }: CheckSvgProps) {
return (
<svg
<Icon
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
@ -23,7 +22,7 @@ export function CheckSvg({ width, height, className }: CheckSvgProps) {
clipRule="evenodd"
d="M7.99992 14.6668C4.31802 14.6668 1.33325 11.6821 1.33325 8.00016C1.33325 4.31826 4.31802 1.3335 7.99992 1.3335C11.6818 1.3335 14.6666 4.31826 14.6666 8.00016C14.6666 11.6821 11.6818 14.6668 7.99992 14.6668ZM7.99992 13.6668C4.8703 13.6668 2.33325 11.1298 2.33325 8.00016C2.33325 4.87055 4.8703 2.3335 7.99992 2.3335C11.1295 2.3335 13.6666 4.87055 13.6666 8.00016C13.6666 11.1298 11.1295 13.6668 7.99992 13.6668Z"
/>
</svg>
</Icon>
);
}
@ -31,12 +30,14 @@ export const CheckIcon = () => {
return <Icon width={16} height={16} />;
};
const Icon = styled(CheckSvg)`
& > path {
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
}
&:hover > path {
&:hover {
fill: ${({ theme }) => theme.bodyBackgroundColor};
}
&.accept {
fill: ${({ theme }) => theme.greenColor};
}
`;

View File

@ -9,7 +9,7 @@ type ClearSvgProps = {
export function ClearSvg({ height, width, className }: ClearSvgProps) {
return (
<svg
<Icon
width={width}
height={height}
viewBox="0 0 16 16"
@ -22,7 +22,7 @@ export function ClearSvg({ height, width, className }: ClearSvgProps) {
clipRule="evenodd"
d="M7.99992 14.6668C11.6818 14.6668 14.6666 11.6821 14.6666 8.00016C14.6666 4.31826 11.6818 1.3335 7.99992 1.3335C4.31802 1.3335 1.33325 4.31826 1.33325 8.00016C1.33325 11.6821 4.31802 14.6668 7.99992 14.6668ZM7.99992 13.6668C11.1295 13.6668 13.6666 11.1298 13.6666 8.00016C13.6666 4.87055 11.1295 2.3335 7.99992 2.3335C4.87031 2.3335 2.33325 4.87055 2.33325 8.00016C2.33325 11.1298 4.87031 13.6668 7.99992 13.6668Z"
/>
</svg>
</Icon>
);
}
@ -30,10 +30,8 @@ export const ClearIcon = () => {
return <Icon width={16} height={16} />;
};
const Icon = styled(ClearSvg)`
& > path {
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
}
&.profile {
fill: ${({ theme }) => theme.secondary};
@ -47,7 +45,7 @@ const Icon = styled(ClearSvg)`
fill: ${({ theme }) => theme.bodyBackgroundColor};
}
&:hover > path {
fill: ${({ theme }) => theme.bodyBackgroundColor};
&.decline {
fill: ${({ theme }) => theme.redColor};
}
`;

View File

@ -0,0 +1,24 @@
import React from "react";
import styled from "styled-components";
export const HideIcon = () => (
<Icon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.96663 7.47938C5.2132 7.67115 5.22516 8.03942 4.99981 8.25572C3.67993 9.52258 2.74867 10.8323 2.30151 11.5167C2.10783 11.8131 2.10783 12.1868 2.30151 12.4832C3.33634 14.067 6.96373 19 12.0005 19C13.9073 19 15.6121 18.293 17.0507 17.3284C17.2343 17.2053 17.4767 17.2094 17.6512 17.3452L20.5401 19.592C20.867 19.8464 21.3382 19.7874 21.5925 19.4605C21.8468 19.1335 21.7879 18.6623 21.461 18.408L3.46098 4.40802C3.13402 4.15372 2.66281 4.21262 2.40851 4.53958C2.15421 4.86654 2.21311 5.33775 2.54007 5.59205L4.96663 7.47938ZM15.5618 15.72C15.8326 15.9307 15.8154 16.3454 15.516 16.5128C14.4352 17.1172 13.2541 17.5 12.0005 17.5C9.98328 17.5 8.15372 16.5088 6.6278 15.1978C5.82477 14.5078 5.14518 13.7624 4.6125 13.1051C4.08889 12.459 4.08889 11.5409 4.6125 10.8948C5.14518 10.2375 5.82477 9.49209 6.6278 8.80216C6.63874 8.79276 6.65478 8.79238 6.66617 8.80124L8.11118 9.92514C8.30075 10.0726 8.3554 10.3329 8.26873 10.5568C8.09549 11.0045 8.00049 11.4912 8.00049 12C8.00049 14.2091 9.79135 16 12.0005 16C12.864 16 13.6636 15.7264 14.3173 15.2611C14.5129 15.1219 14.7786 15.1109 14.9681 15.2583L15.5618 15.72ZM9.50916 11.7902C9.53711 11.4537 9.92818 11.3384 10.1947 11.5457L12.8853 13.6384C13.1517 13.8456 13.1365 14.2532 12.8175 14.3634C12.5615 14.4519 12.2866 14.5 12.0005 14.5C10.6198 14.5 9.50049 13.3807 9.50049 12C9.50049 11.9293 9.50342 11.8594 9.50916 11.7902Z"
/>
<path d="M8.85753 6.41675C8.56645 6.19035 8.61339 5.73908 8.95606 5.60284C9.89767 5.22846 10.9163 5 12.0003 5C17.0371 5 20.6645 9.93293 21.6993 11.5167C21.893 11.8132 21.893 12.1868 21.6993 12.4833C21.3897 12.9571 20.8481 13.7307 20.1061 14.5822C19.9349 14.7787 19.6402 14.8032 19.4344 14.6432L19.0412 14.3374C18.8097 14.1573 18.7827 13.8179 18.9753 13.5967C19.1214 13.429 19.2592 13.2645 19.3883 13.1052C19.9119 12.459 19.9119 11.541 19.3883 10.8948C18.8556 10.2375 18.1761 9.49212 17.373 8.80219C15.8471 7.49116 14.0175 6.5 12.0003 6.5C11.2517 6.5 10.529 6.63648 9.83805 6.87515C9.672 6.93251 9.48731 6.90658 9.34863 6.79872L8.85753 6.41675Z" />
<path d="M15.8618 10.9529C15.9709 11.3561 15.5158 11.5954 15.1862 11.339L13.7844 10.2487C13.6279 10.0894 13.4503 9.95089 13.2561 9.83783L11.8057 8.70975C11.4964 8.46923 11.6085 8 12.0002 8C13.8471 8 15.4016 9.25163 15.8618 10.9529Z" />
</Icon>
);
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
`;

View File

@ -0,0 +1,16 @@
import styled from "styled-components";
export const Icon = styled.div`
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: end;
border-radius: 50%;
background-color: #bcbdff;
background-size: contain;
background-position: center;
flex-shrink: 0;
position: relative;
cursor: pointer;
`;

View File

@ -0,0 +1,43 @@
import React from "react";
import styled from "styled-components";
interface ReadIconProps {
isRead?: boolean;
}
export const ReadIcon = ({ isRead }: ReadIconProps) => {
return (
<Icon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`${isRead && "read"}`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22.863 4.42224C23.2076 4.652 23.3008 5.11765 23.071 5.4623L13.197 19.4224C13.0723 19.6094 12.8704 19.7306 12.6468 19.7527C12.4232 19.7749 12.2015 19.6956 12.0426 19.5367L6.04259 13.5367C5.7497 13.2438 5.7497 12.7689 6.04259 12.476C6.33548 12.1832 6.81036 12.1832 7.10325 12.476L12.4564 17.8291L21.8229 4.63025C22.0527 4.2856 22.5183 4.19247 22.863 4.42224Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.8545 4.38389C18.1916 4.62465 18.2696 5.09306 18.0289 5.43012L12.0514 13.7988C11.8106 14.1359 11.3422 14.214 11.0052 13.9732C10.6681 13.7325 10.59 13.2641 10.8308 12.927L16.8083 4.55828C17.049 4.22122 17.5174 4.14314 17.8545 4.38389Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.0183 12.48C1.31 12.1859 1.78487 12.184 2.07895 12.4757L8.09656 18.4446C8.39064 18.7363 8.39257 19.2112 8.10087 19.5053C7.80917 19.7994 7.3343 19.8013 7.04022 19.5096L1.02261 13.5407C0.728529 13.249 0.7266 12.7741 1.0183 12.48Z"
/>
</Icon>
);
};
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
&.read {
fill: ${({ theme }) => theme.secondary};
}
`;

View File

@ -0,0 +1,19 @@
import React from "react";
import styled from "styled-components";
export const ReplyIcon = () => (
<Icon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 5.08946V3.62446C8 3.00346 8.373 2.81246 8.824 3.20246L12.828 6.64746C13.055 6.84246 13.054 7.15746 12.827 7.35146L8.822 10.7975C8.373 11.1855 8 10.9955 8 10.3745V8.99946C5.498 8.99946 4.183 10.8865 3.524 12.5315C3.421 12.7885 3.271 12.7885 3.2 12.5205C3.071 12.0355 3 11.5255 3 10.9995C3 8.02646 5.164 5.56546 8 5.08946Z" />
</Icon>
);
const Icon = styled.svg`
fill: ${({ theme }) => theme.secondary};
flex-shrink: 0;
`;

View File

@ -0,0 +1,27 @@
import React from "react";
import styled from "styled-components";
export const ShowIcon = () => (
<Icon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16.0005 12C16.0005 14.2091 14.2096 16 12.0005 16C9.79135 16 8.00049 14.2091 8.00049 12C8.00049 9.79086 9.79135 8 12.0005 8C14.2096 8 16.0005 9.79086 16.0005 12ZM14.5005 12C14.5005 13.3807 13.3812 14.5 12.0005 14.5C10.6198 14.5 9.50049 13.3807 9.50049 12C9.50049 10.6193 10.6198 9.5 12.0005 9.5C13.3812 9.5 14.5005 10.6193 14.5005 12Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.30151 12.4833C3.33634 14.0671 6.96373 19 12.0005 19C17.0373 19 20.6647 14.0671 21.6996 12.4833C21.8932 12.1868 21.8932 11.8132 21.6996 11.5167C20.6647 9.93293 17.0373 5 12.0005 5C6.96373 5 3.33634 9.93293 2.30151 11.5167C2.10783 11.8132 2.10783 12.1868 2.30151 12.4833ZM12.0005 17.5C9.98328 17.5 8.15372 16.5088 6.6278 15.1978C5.82477 14.5079 5.14518 13.7625 4.6125 13.1052C4.08889 12.459 4.08889 11.541 4.6125 10.8948C5.14518 10.2375 5.82477 9.49212 6.6278 8.80219C8.15372 7.49115 9.98328 6.5 12.0005 6.5C14.0178 6.5 15.8473 7.49116 17.3733 8.80219C18.1763 9.49212 18.8559 10.2375 19.3886 10.8948C19.9122 11.541 19.9122 12.459 19.3886 13.1052C18.8559 13.7625 18.1763 14.5079 17.3733 15.1978C15.8473 16.5088 14.0178 17.5 12.0005 17.5Z"
/>
</Icon>
);
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
`;

View File

@ -3,8 +3,8 @@ import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { Contact } from "../../models/Contact";
import { Icon } from "../Chat/ChatMessages";
import { ContactMenu } from "../Form/ContactMenu";
import { Icon } from "../Icons/Icon";
import { UserIcon } from "../Icons/UserIcon";
interface MemberProps {

View File

@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
@ -40,6 +41,8 @@ export const ProfileModal = () => {
[props]
);
const { setActivities } = useActivities();
const identity = useIdentity();
const isUser = useMemo(
() => id === bufToHex(identity.publicKey),
@ -47,12 +50,14 @@ export const ProfileModal = () => {
);
const [renaming, setRenaming] = useState(renamingState ?? false);
useEffect(() => {
setRenaming(renamingState ?? false);
}, [renamingState]);
const [request, setRequest] = useState("");
const [requestCreation, setRequestCreation] = useState(requestState ?? false);
useEffect(() => {
setRequestCreation(requestState ?? false);
}, [requestState]);
@ -183,7 +188,19 @@ export const ProfileModal = () => {
<Btn
disabled={!request}
onClick={() => {
setIsUserFriend(true),
setActivities((prev) => [
...prev,
{
id: id + request,
type: "request",
isRead: true,
date: new Date(),
user: id,
request: request,
requestType: "outcome",
status: "sent",
},
]),
setRequestCreation(false),
setRequest("");
}}

View File

@ -2,6 +2,7 @@ import React, { useRef } from "react";
import { ThemeProvider } from "styled-components";
import styled from "styled-components";
import { ActivityProvider } from "../contexts/activityProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { ModalProvider } from "../contexts/modalProvider";
import { NarrowProvider } from "../contexts/narrowProvider";
@ -28,11 +29,13 @@ export function ReactChat({
<NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<ModalProvider>
<ActivityProvider>
<Wrapper ref={ref}>
<GlobalStyle />
<ChatLoader communityKey={communityKey} />
<div id="modal-root" />
</Wrapper>
</ActivityProvider>
</ModalProvider>
</FetchMetadataProvider>
</NarrowProvider>

View File

@ -0,0 +1,29 @@
import React, { createContext, useContext, useState } from "react";
import { Activity } from "../models/Activity";
const ActivityContext = createContext<{
activities: Activity[];
setActivities: React.Dispatch<React.SetStateAction<Activity[]>>;
}>({
activities: [],
setActivities: () => undefined,
});
export function useActivities() {
return useContext(ActivityContext);
}
interface ActivityProviderProps {
children: React.ReactNode;
}
export function ActivityProvider({ children }: ActivityProviderProps) {
const [activities, setActivities] = useState<Activity[]>([]);
return (
<ActivityContext.Provider
value={{ activities, setActivities }}
children={children}
/>
);
}

View File

@ -0,0 +1,20 @@
import { ChannelData } from "./ChannelData";
import { ChatMessage } from "./ChatMessage";
export type Activity = {
id: string;
type: "mention" | "request" | "reply";
isRead?: boolean;
date: Date;
user: string;
message?: ChatMessage;
channel?: ChannelData;
request?: string;
requestType?: "outcome" | "income";
status?: "sent" | "accepted" | "declined" | "blocked";
quote?: ChatMessage;
};
export type Activities = {
[id: string]: Activity;
};

View File

@ -19,6 +19,7 @@ export type Theme = {
skeletonLight: string;
skeletonDark: string;
redColor: string;
greenColor: string;
mentionColor: string;
mentionHover: string;
mentionBg: string;
@ -46,6 +47,7 @@ export const lightTheme: Theme = {
skeletonLight: "#F6F8FA",
skeletonDark: "#E9EDF1",
redColor: "#FF2D55",
greenColor: "#4EBC60",
mentionColor: "#0DA4C9",
mentionHover: "#BDE7F2",
mentionBg: "#E5F8FD",
@ -73,6 +75,7 @@ export const darkTheme: Theme = {
skeletonLight: "#2E2F31",
skeletonDark: "#141414",
redColor: "#FF5C7B",
greenColor: "#60C370",
mentionColor: "#51D0F0",
mentionHover: "#004E60",
mentionBg: "#004050",