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:
parent
4852d90546
commit
77dfd154b2
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
|
@ -28,5 +28,5 @@ export const useContextMenu = (elementId: string) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return { showMenu };
|
return { showMenu, setShowMenu };
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue