UI improvements (#135)

* Fix channel name styles

* Add active class to group icon

* Fix channel description

* Hide member category

* Fix community name alignment

* Add unmuting

* Add group unmuting

* Add user name to empty text

* Fix group description

* Add closing dropdown on click outside

* Add memoization

* Change useClickOutside
This commit is contained in:
Maria Rushkova 2021-11-26 10:23:52 +01:00 committed by GitHub
parent 4852d90546
commit 77dfd154b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 105 additions and 38 deletions

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useNarrow } from "../../contexts/narrowProvider"; import { useNarrow } from "../../contexts/narrowProvider";
import { ChannelData } from "../../models/ChannelData"; import { ChannelData } from "../../models/ChannelData";
import { GroupIcon } from "../Icons/GroupIcon"; import { GroupIcon } from "../Icons/GroupIcon";
@ -14,12 +15,13 @@ function RenderChannelName({
channel: ChannelData; channel: ChannelData;
className?: string; className?: string;
}) { }) {
const { activeChannel } = useMessengerContext();
switch (channel.type) { switch (channel.type) {
case "group": case "group":
return ( return (
<div className={className}> <div className={className}>
<GroupIcon /> <GroupIcon activeView={channel.id === activeChannel.id} />
{channel.name} {` ${channel.name}`}
</div> </div>
); );
case "channel": case "channel":
@ -77,7 +79,7 @@ export function Channel({
<ChannelTextInfo> <ChannelTextInfo>
<ChannelName <ChannelName
channel={channel} channel={channel}
active={isActive || narrow} active={isActive || activeView || narrow}
muted={channel?.isMuted} muted={channel?.isMuted}
notified={notified} notified={notified}
/> />
@ -89,7 +91,7 @@ export function Channel({
{!activeView && !!mention && mention > 0 && !channel?.isMuted && ( {!activeView && !!mention && mention > 0 && !channel?.isMuted && (
<NotificationBagde>{mention}</NotificationBagde> <NotificationBagde>{mention}</NotificationBagde>
)} )}
{channel?.isMuted && <MutedIcon />} {channel?.isMuted && !activeView && <MutedIcon />}
</ChannelWrapper> </ChannelWrapper>
); );
} }
@ -101,6 +103,7 @@ const ChannelWrapper = styled.div<{ isNarrow?: boolean }>`
padding: 8px; padding: 8px;
cursor: pointer; cursor: pointer;
width: ${({ isNarrow }) => (isNarrow ? "calc(100% - 162px)" : "100%")}; width: ${({ isNarrow }) => (isNarrow ? "calc(100% - 162px)" : "100%")};
&.active { &.active {
background-color: ${({ theme }) => theme.activeChannelBackground}; background-color: ${({ theme }) => theme.activeChannelBackground};
border-radius: 8px; border-radius: 8px;
@ -136,7 +139,7 @@ export const ChannelLogo = styled.div<{ icon?: string }>`
background-color: ${({ theme }) => theme.iconColor}; background-color: ${({ theme }) => theme.iconColor};
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
backgroundimage: ${({ icon }) => icon && `url(${icon}`}; background-image: ${({ icon }) => icon && `url(${icon}`};
color: ${({ theme }) => theme.iconTextColor}; color: ${({ theme }) => theme.iconTextColor};
&.active { &.active {

View File

@ -1,6 +1,8 @@
import React from "react"; import React from "react";
import { utils } from "status-communities/dist/cjs";
import styled from "styled-components"; import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { ChannelData } from "../../models/ChannelData"; import { ChannelData } from "../../models/ChannelData";
import { textMediumStyles } from "../Text"; import { textMediumStyles } from "../Text";
@ -12,6 +14,7 @@ type EmptyChannelProps = {
export function EmptyChannel({ channel }: EmptyChannelProps) { export function EmptyChannel({ channel }: EmptyChannelProps) {
const groupName = channel.name.split(", "); const groupName = channel.name.split(", ");
const identity = useIdentity();
return ( return (
<Wrapper> <Wrapper>
@ -31,7 +34,7 @@ export function EmptyChannel({ channel }: EmptyChannelProps) {
</EmptyText> </EmptyText>
) : channel.type === "group" ? ( ) : channel.type === "group" ? (
<EmptyTextGroup> <EmptyTextGroup>
You created a group with{" "} <span>{utils.bufToHex(identity.publicKey)}</span> created a group with{" "}
<span>{groupName.slice(groupName.length - 1)}</span> and{" "} <span>{groupName.slice(groupName.length - 1)}</span> and{" "}
<span>{groupName.at(-1)}</span> <span>{groupName.at(-1)}</span>
</EmptyTextGroup> </EmptyTextGroup>
@ -88,5 +91,7 @@ const EmptyText = styled.p`
`; `;
const EmptyTextGroup = styled(EmptyText)` const EmptyTextGroup = styled(EmptyText)`
word-break: break-all; & > span {
word-break: break-all;
}
`; `;

View File

@ -45,6 +45,7 @@ export function ChatCreation({ editGroup }: ChatCreationProps) {
id: group.join(""), id: group.join(""),
name: group.join(", "), name: group.join(", "),
type: "group", type: "group",
description: `${group.length + 1} members`,
}) })
: setChannel({ : setChannel({
id: group[0], id: group[0],

View File

@ -124,11 +124,11 @@ const MessageImage = styled.img`
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 16px; border-radius: 16px;
cursor; pointer; cursor: pointer;
`; `;
const PreviewSiteNameWrapper = styled.div` const PreviewSiteNameWrapper = styled.div`
font-family: Inter; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
font-size: 12px; font-size: 12px;

View File

@ -70,6 +70,7 @@ const Name = styled.p`
font-weight: 500; font-weight: 500;
text-align: left; text-align: left;
color: ${({ theme }) => theme.primary}; color: ${({ theme }) => theme.primary};
white-space: nowrap;
${textMediumStyles} ${textMediumStyles}
`; `;

View File

@ -31,8 +31,9 @@ export const ChannelMenu = ({
const narrow = useNarrow(); const narrow = useNarrow();
const { clearNotifications, removeChannel } = useMessengerContext(); const { clearNotifications, removeChannel } = useMessengerContext();
const { setModal } = useModal(EditModalName); const { setModal } = useModal(EditModalName);
return ( return (
<DropdownMenu> <DropdownMenu closeMenu={setShowChannelMenu}>
{narrow && ( {narrow && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
@ -63,12 +64,15 @@ export const ChannelMenu = ({
)} )}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
channel.isMuted = true; channel.isMuted = !channel.isMuted;
setShowChannelMenu(false); setShowChannelMenu(false);
}} }}
> >
<MuteSvg width={16} height={16} /> <MuteSvg width={16} height={16} />
<MenuText>Mute Chat</MenuText> <MenuText>
{(channel.isMuted ? "Unmute" : "Mute") +
(channel.type === "group" ? " Group" : " Chat")}
</MenuText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => clearNotifications(channel.id)}> <MenuItem onClick={() => clearNotifications(channel.id)}>
<CheckSvg width={16} height={16} /> <CheckSvg width={16} height={16} />

View File

@ -37,7 +37,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
if (!contact) return null; if (!contact) return null;
return ( return (
<ContactDropdown> <ContactDropdown closeMenu={setShowMenu}>
<ContactInfo> <ContactInfo>
<UserIcon /> <UserIcon />
<UserNameWrapper> <UserNameWrapper>

View File

@ -1,16 +1,25 @@
import React, { ReactNode } from "react"; import React, { ReactNode, useRef } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useClickOutside } from "../../hooks/useClickOutside";
import { textSmallStyles } from "../Text"; import { textSmallStyles } from "../Text";
type DropdownMenuProps = { type DropdownMenuProps = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
closeMenu: (val: boolean) => void;
}; };
export function DropdownMenu({ children, className }: DropdownMenuProps) { export function DropdownMenu({
children,
className,
closeMenu,
}: DropdownMenuProps) {
const ref = useRef(null);
useClickOutside(ref, () => closeMenu(false));
return ( return (
<MenuBlock className={className}> <MenuBlock className={className} ref={ref}>
<MenuList>{children}</MenuList> <MenuList>{children}</MenuList>
</MenuBlock> </MenuBlock>
); );

View File

@ -14,10 +14,10 @@ interface ImageMenuProps {
} }
export const ImageMenu = ({ imageId }: ImageMenuProps) => { export const ImageMenu = ({ imageId }: ImageMenuProps) => {
const { showMenu } = useContextMenu(imageId); const { showMenu, setShowMenu } = useContextMenu(imageId);
return showMenu ? ( return showMenu ? (
<ImageDropdown> <ImageDropdown closeMenu={setShowMenu}>
<MenuItem onClick={() => copyImg(imageId)}> <MenuItem onClick={() => copyImg(imageId)}>
<CopySvg height={16} width={16} /> <MenuText>Copy image</MenuText> <CopySvg height={16} width={16} /> <MenuText>Copy image</MenuText>
</MenuItem> </MenuItem>

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
export const GroupIcon = () => { interface GroupIconProps {
activeView?: boolean;
}
export const GroupIcon = ({ activeView }: GroupIconProps) => {
return ( return (
<Icon <Icon
width="14" width="14"
@ -9,6 +13,7 @@ export const GroupIcon = () => {
viewBox="0 0 14 10" viewBox="0 0 14 10"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={`${activeView && "active"}`}
> >
<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="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.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" />
@ -19,7 +24,9 @@ export const GroupIcon = () => {
}; };
const Icon = styled.svg` const Icon = styled.svg`
& > path { fill: ${({ theme }) => theme.secondary};
fill: ${({ theme }) => theme.secondary};
&.active {
fill: ${({ theme }) => theme.primary};
} }
`; `;

View File

@ -30,7 +30,7 @@ export function Member({
id: contact.id, id: contact.id,
name: contact.customName ?? contact.trueName, name: contact.customName ?? contact.trueName,
type: "dm", type: "dm",
description: "DM", description: "Contact",
members: [contact], members: [contact],
}); });
}; };

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useMemo } from "react";
import { utils } from "status-communities/dist/cjs"; import { utils } from "status-communities/dist/cjs";
import { bufToHex } from "status-communities/dist/cjs/utils"; import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components"; import styled from "styled-components";
@ -16,6 +16,21 @@ interface MembersListProps {
export function MembersList({ switchShowMembers }: MembersListProps) { export function MembersList({ switchShowMembers }: MembersListProps) {
const { contacts } = useMessengerContext(); const { contacts } = useMessengerContext();
const identity = useIdentity(); const identity = useIdentity();
const userContacts = useMemo(
() =>
Object.values(contacts).filter(
(e) => e.id != bufToHex(identity.publicKey)
),
[contacts, identity]
);
const onlineContacts = useMemo(
() => userContacts.filter((e) => e.online),
[userContacts]
);
const offlineContacts = useMemo(
() => userContacts.filter((e) => !e.online),
[userContacts]
);
return ( return (
<MembersListWrap> <MembersListWrap>
@ -28,12 +43,10 @@ export function MembersList({ switchShowMembers }: MembersListProps) {
<MemberName>{utils.bufToHex(identity.publicKey)}</MemberName> <MemberName>{utils.bufToHex(identity.publicKey)}</MemberName>
</MemberData> </MemberData>
</MemberCategory> </MemberCategory>
<MemberCategory> {onlineContacts.length > 0 && (
<MemberCategoryName>Online</MemberCategoryName> <MemberCategory>
{Object.values(contacts) <MemberCategoryName>Online</MemberCategoryName>
.filter((e) => e.id != bufToHex(identity.publicKey)) {onlineContacts.map((contact) => (
.filter((e) => e.online)
.map((contact) => (
<Member <Member
key={contact.id} key={contact.id}
contact={contact} contact={contact}
@ -41,13 +54,12 @@ export function MembersList({ switchShowMembers }: MembersListProps) {
switchShowMembers={switchShowMembers} switchShowMembers={switchShowMembers}
/> />
))} ))}
</MemberCategory> </MemberCategory>
<MemberCategory> )}
<MemberCategoryName>Offline</MemberCategoryName> {offlineContacts.length > 0 && (
{Object.values(contacts) <MemberCategory>
.filter((e) => e.id != bufToHex(identity.publicKey)) <MemberCategoryName>Offline</MemberCategoryName>
.filter((e) => !e.online) {offlineContacts.map((contact) => (
.map((contact) => (
<Member <Member
key={contact.id} key={contact.id}
contact={contact} contact={contact}
@ -55,7 +67,8 @@ export function MembersList({ switchShowMembers }: MembersListProps) {
switchShowMembers={switchShowMembers} switchShowMembers={switchShowMembers}
/> />
))} ))}
</MemberCategory> </MemberCategory>
)}
</MembersListWrap> </MembersListWrap>
); );
} }

View File

@ -0,0 +1,24 @@
import { RefObject, useCallback, useEffect } from "react";
export const useClickOutside = (
ref: RefObject<HTMLDivElement>,
callback: () => void,
deps?: any[]
) => {
const handleClick = useCallback(
(e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) {
callback();
}
},
[ref, callback]
);
useEffect(() => {
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, deps);
};

View File

@ -28,5 +28,5 @@ export const useContextMenu = (elementId: string) => {
}; };
}); });
return { showMenu }; return { showMenu, setShowMenu };
}; };