Use tabWidth 4 without actual tabs.

This commit is contained in:
Paul
2021-07-05 11:25:20 +01:00
parent a9ce64c9fe
commit 14809f1989
180 changed files with 16619 additions and 16622 deletions

View File

@@ -13,84 +13,84 @@ import UserIcon from "../common/user/UserIcon";
import IconButton from "../ui/IconButton";
const NavigationBase = styled.div`
z-index: 100;
height: 50px;
display: flex;
background: var(--secondary-background);
z-index: 100;
height: 50px;
display: flex;
background: var(--secondary-background);
`;
const Button = styled.a<{ active: boolean }>`
flex: 1;
flex: 1;
> a,
> div,
> a > div {
width: 100%;
height: 100%;
}
> a,
> div,
> a > div {
width: 100%;
height: 100%;
}
${(props) =>
props.active &&
css`
background: var(--hover);
`}
${(props) =>
props.active &&
css`
background: var(--hover);
`}
`;
interface Props {
lastOpened: LastOpened;
lastOpened: LastOpened;
}
export function BottomNavigation({ lastOpened }: Props) {
const user = useSelf();
const history = useHistory();
const path = useLocation().pathname;
const user = useSelf();
const history = useHistory();
const path = useLocation().pathname;
const channel_id = lastOpened["home"];
const channel_id = lastOpened["home"];
const friendsActive = path.startsWith("/friends");
const settingsActive = path.startsWith("/settings");
const homeActive = !(friendsActive || settingsActive);
const friendsActive = path.startsWith("/friends");
const settingsActive = path.startsWith("/settings");
const homeActive = !(friendsActive || settingsActive);
return (
<NavigationBase>
<Button active={homeActive}>
<IconButton
onClick={() => {
if (settingsActive) {
if (history.length > 0) {
history.goBack();
}
}
return (
<NavigationBase>
<Button active={homeActive}>
<IconButton
onClick={() => {
if (settingsActive) {
if (history.length > 0) {
history.goBack();
}
}
if (channel_id) {
history.push(`/channel/${channel_id}`);
} else {
history.push("/");
}
}}>
<Message size={24} />
</IconButton>
</Button>
<Button active={friendsActive}>
<ConditionalLink active={friendsActive} to="/friends">
<IconButton>
<Group size={25} />
</IconButton>
</ConditionalLink>
</Button>
<Button active={settingsActive}>
<ConditionalLink active={settingsActive} to="/settings">
<IconButton>
<UserIcon target={user} size={26} status={true} />
</IconButton>
</ConditionalLink>
</Button>
</NavigationBase>
);
if (channel_id) {
history.push(`/channel/${channel_id}`);
} else {
history.push("/");
}
}}>
<Message size={24} />
</IconButton>
</Button>
<Button active={friendsActive}>
<ConditionalLink active={friendsActive} to="/friends">
<IconButton>
<Group size={25} />
</IconButton>
</ConditionalLink>
</Button>
<Button active={settingsActive}>
<ConditionalLink active={settingsActive} to="/settings">
<IconButton>
<UserIcon target={user} size={26} status={true} />
</IconButton>
</ConditionalLink>
</Button>
</NavigationBase>
);
}
export default connectState(BottomNavigation, (state) => {
return {
lastOpened: state.lastOpened,
};
return {
lastOpened: state.lastOpened,
};
});

View File

@@ -6,27 +6,27 @@ import ServerListSidebar from "./left/ServerListSidebar";
import ServerSidebar from "./left/ServerSidebar";
export default function LeftSidebar() {
return (
<SidebarBase>
<Switch>
<Route path="/settings" />
<Route path="/server/:server/channel/:channel">
<ServerListSidebar />
<ServerSidebar />
</Route>
<Route path="/server/:server">
<ServerListSidebar />
<ServerSidebar />
</Route>
<Route path="/channel/:channel">
<ServerListSidebar />
<HomeSidebar />
</Route>
<Route path="/">
<ServerListSidebar />
<HomeSidebar />
</Route>
</Switch>
</SidebarBase>
);
return (
<SidebarBase>
<Switch>
<Route path="/settings" />
<Route path="/server/:server/channel/:channel">
<ServerListSidebar />
<ServerSidebar />
</Route>
<Route path="/server/:server">
<ServerListSidebar />
<ServerSidebar />
</Route>
<Route path="/channel/:channel">
<ServerListSidebar />
<HomeSidebar />
</Route>
<Route path="/">
<ServerListSidebar />
<HomeSidebar />
</Route>
</Switch>
</SidebarBase>
);
}

View File

@@ -4,16 +4,16 @@ import SidebarBase from "./SidebarBase";
import MemberSidebar from "./right/MemberSidebar";
export default function RightSidebar() {
return (
<SidebarBase>
<Switch>
<Route path="/server/:server/channel/:channel">
<MemberSidebar />
</Route>
<Route path="/channel/:channel">
<MemberSidebar />
</Route>
</Switch>
</SidebarBase>
);
return (
<SidebarBase>
<Switch>
<Route path="/server/:server/channel/:channel">
<MemberSidebar />
</Route>
<Route path="/channel/:channel">
<MemberSidebar />
</Route>
</Switch>
</SidebarBase>
);
}

View File

@@ -3,36 +3,36 @@ import styled, { css } from "styled-components";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
export default styled.div`
height: 100%;
display: flex;
user-select: none;
flex-direction: row;
align-items: stretch;
height: 100%;
display: flex;
user-select: none;
flex-direction: row;
align-items: stretch;
`;
export const GenericSidebarBase = styled.div<{ padding?: boolean }>`
height: 100%;
width: 240px;
display: flex;
flex-shrink: 0;
flex-direction: column;
background: var(--secondary-background);
border-end-start-radius: 8px;
height: 100%;
width: 240px;
display: flex;
flex-shrink: 0;
flex-direction: column;
background: var(--secondary-background);
border-end-start-radius: 8px;
${(props) =>
props.padding &&
isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
${(props) =>
props.padding &&
isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`;
export const GenericSidebarList = styled.div`
padding: 6px;
flex-grow: 1;
overflow-y: scroll;
padding: 6px;
flex-grow: 1;
overflow-y: scroll;
> img {
width: 100%;
}
> img {
width: 100%;
}
`;

View File

@@ -20,204 +20,204 @@ import IconButton from "../../ui/IconButton";
import { Children } from "../../../types/Preact";
type CommonProps = Omit<
JSX.HTMLAttributes<HTMLDivElement>,
"children" | "as"
JSX.HTMLAttributes<HTMLDivElement>,
"children" | "as"
> & {
active?: boolean;
alert?: "unread" | "mention";
alertCount?: number;
active?: boolean;
alert?: "unread" | "mention";
alertCount?: number;
};
type UserProps = CommonProps & {
user: Users.User;
context?: Channels.Channel;
channel?: Channels.DirectMessageChannel;
user: Users.User;
context?: Channels.Channel;
channel?: Channels.DirectMessageChannel;
};
export function UserButton(props: UserProps) {
const { active, alert, alertCount, user, context, channel, ...divProps } =
props;
const { openScreen } = useIntermediate();
const { active, alert, alertCount, user, context, channel, ...divProps } =
props;
const { openScreen } = useIntermediate();
return (
<div
{...divProps}
className={classNames(styles.item, styles.user)}
data-active={active}
data-alert={typeof alert === "string"}
data-online={
typeof channel !== "undefined" ||
(user.online &&
user.status?.presence !== Users.Presence.Invisible)
}
onContextMenu={attachContextMenu("Menu", {
user: user._id,
channel: channel?._id,
unread: alert,
contextualChannel: context?._id,
})}>
<UserIcon
className={styles.avatar}
target={user}
size={32}
status
/>
<div className={styles.name}>
<div>{user.username}</div>
{
<div className={styles.subText}>
{channel?.last_message && alert ? (
channel.last_message.short
) : (
<UserStatus user={user} />
)}
</div>
}
</div>
<div className={styles.button}>
{context?.channel_type === "Group" &&
context.owner === user._id && (
<Localizer>
<Tooltip
content={<Text id="app.main.groups.owner" />}>
<Crown size={20} />
</Tooltip>
</Localizer>
)}
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
{!isTouchscreenDevice && channel && (
<IconButton
className={styles.icon}
onClick={(e) =>
stopPropagation(e) &&
openScreen({
id: "special_prompt",
type: "close_dm",
target: channel,
})
}>
<X size={24} />
</IconButton>
)}
</div>
</div>
);
return (
<div
{...divProps}
className={classNames(styles.item, styles.user)}
data-active={active}
data-alert={typeof alert === "string"}
data-online={
typeof channel !== "undefined" ||
(user.online &&
user.status?.presence !== Users.Presence.Invisible)
}
onContextMenu={attachContextMenu("Menu", {
user: user._id,
channel: channel?._id,
unread: alert,
contextualChannel: context?._id,
})}>
<UserIcon
className={styles.avatar}
target={user}
size={32}
status
/>
<div className={styles.name}>
<div>{user.username}</div>
{
<div className={styles.subText}>
{channel?.last_message && alert ? (
channel.last_message.short
) : (
<UserStatus user={user} />
)}
</div>
}
</div>
<div className={styles.button}>
{context?.channel_type === "Group" &&
context.owner === user._id && (
<Localizer>
<Tooltip
content={<Text id="app.main.groups.owner" />}>
<Crown size={20} />
</Tooltip>
</Localizer>
)}
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
{!isTouchscreenDevice && channel && (
<IconButton
className={styles.icon}
onClick={(e) =>
stopPropagation(e) &&
openScreen({
id: "special_prompt",
type: "close_dm",
target: channel,
})
}>
<X size={24} />
</IconButton>
)}
</div>
</div>
);
}
type ChannelProps = CommonProps & {
channel: Channels.Channel & { unread?: string };
user?: Users.User;
compact?: boolean;
channel: Channels.Channel & { unread?: string };
user?: Users.User;
compact?: boolean;
};
export function ChannelButton(props: ChannelProps) {
const { active, alert, alertCount, channel, user, compact, ...divProps } =
props;
const { active, alert, alertCount, channel, user, compact, ...divProps } =
props;
if (channel.channel_type === "SavedMessages") throw "Invalid channel type.";
if (channel.channel_type === "DirectMessage") {
if (typeof user === "undefined") throw "No user provided.";
return <UserButton {...{ active, alert, channel, user }} />;
}
if (channel.channel_type === "SavedMessages") throw "Invalid channel type.";
if (channel.channel_type === "DirectMessage") {
if (typeof user === "undefined") throw "No user provided.";
return <UserButton {...{ active, alert, channel, user }} />;
}
const { openScreen } = useIntermediate();
const { openScreen } = useIntermediate();
return (
<div
{...divProps}
data-active={active}
data-alert={typeof alert === "string"}
aria-label={{}} /*FIXME: ADD ARIA LABEL*/
className={classNames(styles.item, { [styles.compact]: compact })}
onContextMenu={attachContextMenu("Menu", {
channel: channel._id,
unread: typeof channel.unread !== "undefined",
})}>
<ChannelIcon
className={styles.avatar}
target={channel}
size={compact ? 24 : 32}
/>
<div className={styles.name}>
<div>{channel.name}</div>
{channel.channel_type === "Group" && (
<div className={styles.subText}>
{channel.last_message && alert ? (
channel.last_message.short
) : (
<Text
id="quantities.members"
plural={channel.recipients.length}
fields={{ count: channel.recipients.length }}
/>
)}
</div>
)}
</div>
<div className={styles.button}>
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
{!isTouchscreenDevice && channel.channel_type === "Group" && (
<IconButton
className={styles.icon}
onClick={() =>
openScreen({
id: "special_prompt",
type: "leave_group",
target: channel,
})
}>
<X size={24} />
</IconButton>
)}
</div>
</div>
);
return (
<div
{...divProps}
data-active={active}
data-alert={typeof alert === "string"}
aria-label={{}} /*FIXME: ADD ARIA LABEL*/
className={classNames(styles.item, { [styles.compact]: compact })}
onContextMenu={attachContextMenu("Menu", {
channel: channel._id,
unread: typeof channel.unread !== "undefined",
})}>
<ChannelIcon
className={styles.avatar}
target={channel}
size={compact ? 24 : 32}
/>
<div className={styles.name}>
<div>{channel.name}</div>
{channel.channel_type === "Group" && (
<div className={styles.subText}>
{channel.last_message && alert ? (
channel.last_message.short
) : (
<Text
id="quantities.members"
plural={channel.recipients.length}
fields={{ count: channel.recipients.length }}
/>
)}
</div>
)}
</div>
<div className={styles.button}>
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
{!isTouchscreenDevice && channel.channel_type === "Group" && (
<IconButton
className={styles.icon}
onClick={() =>
openScreen({
id: "special_prompt",
type: "leave_group",
target: channel,
})
}>
<X size={24} />
</IconButton>
)}
</div>
</div>
);
}
type ButtonProps = CommonProps & {
onClick?: () => void;
children?: Children;
className?: string;
compact?: boolean;
onClick?: () => void;
children?: Children;
className?: string;
compact?: boolean;
};
export default function ButtonItem(props: ButtonProps) {
const {
active,
alert,
alertCount,
onClick,
className,
children,
compact,
...divProps
} = props;
const {
active,
alert,
alertCount,
onClick,
className,
children,
compact,
...divProps
} = props;
return (
<div
{...divProps}
className={classNames(
styles.item,
{ [styles.compact]: compact, [styles.normal]: !compact },
className,
)}
onClick={onClick}
data-active={active}
data-alert={typeof alert === "string"}>
<div className={styles.content}>{children}</div>
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
</div>
);
return (
<div
{...divProps}
className={classNames(
styles.item,
{ [styles.compact]: compact, [styles.normal]: !compact },
className,
)}
onClick={onClick}
data-active={active}
data-alert={typeof alert === "string"}>
<div className={styles.content}>{children}</div>
{alert && (
<div className={styles.alert} data-style={alert}>
{alertCount}
</div>
)}
</div>
);
}

View File

@@ -2,39 +2,39 @@ import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import {
ClientStatus,
StatusContext,
ClientStatus,
StatusContext,
} from "../../../context/revoltjs/RevoltClient";
import Banner from "../../ui/Banner";
export default function ConnectionStatus() {
const status = useContext(StatusContext);
const status = useContext(StatusContext);
if (status === ClientStatus.OFFLINE) {
return (
<Banner>
<Text id="app.special.status.offline" />
</Banner>
);
} else if (status === ClientStatus.DISCONNECTED) {
return (
<Banner>
<Text id="app.special.status.disconnected" />
</Banner>
);
} else if (status === ClientStatus.CONNECTING) {
return (
<Banner>
<Text id="app.special.status.connecting" />
</Banner>
);
} else if (status === ClientStatus.RECONNECTING) {
return (
<Banner>
<Text id="app.special.status.reconnecting" />
</Banner>
);
}
return null;
if (status === ClientStatus.OFFLINE) {
return (
<Banner>
<Text id="app.special.status.offline" />
</Banner>
);
} else if (status === ClientStatus.DISCONNECTED) {
return (
<Banner>
<Text id="app.special.status.disconnected" />
</Banner>
);
} else if (status === ClientStatus.CONNECTING) {
return (
<Banner>
<Text id="app.special.status.connecting" />
</Banner>
);
} else if (status === ClientStatus.RECONNECTING) {
return (
<Banner>
<Text id="app.special.status.reconnecting" />
</Banner>
);
}
return null;
}

View File

@@ -1,8 +1,8 @@
import {
Home,
UserDetail,
Wrench,
Notepad,
Home,
UserDetail,
Wrench,
Notepad,
} from "@styled-icons/boxicons-solid";
import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import { Channels } from "revolt.js/dist/api/objects";
@@ -22,9 +22,9 @@ import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import {
useDMs,
useForceUpdate,
useUsers,
useDMs,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import UserHeader from "../../common/user/UserHeader";
@@ -37,157 +37,157 @@ import ButtonItem, { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus";
type Props = {
unreads: Unreads;
unreads: Unreads;
};
function HomeSidebar(props: Props) {
const { pathname } = useLocation();
const client = useContext(AppContext);
const { channel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate();
const { pathname } = useLocation();
const client = useContext(AppContext);
const { channel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate();
const ctx = useForceUpdate();
const channels = useDMs(ctx);
const ctx = useForceUpdate();
const channels = useDMs(ctx);
const obj = channels.find((x) => x?._id === channel);
if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj });
const obj = channels.find((x) => x?._id === channel);
if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj });
useEffect(() => {
if (!channel) return;
useEffect(() => {
if (!channel) return;
dispatch({
type: "LAST_OPENED_SET",
parent: "home",
child: channel,
});
}, [channel]);
dispatch({
type: "LAST_OPENED_SET",
parent: "home",
child: channel,
});
}, [channel]);
const channelsArr = channels
.filter((x) => x.channel_type !== "SavedMessages")
.map((x) => mapChannelWithUnread(x, props.unreads));
const channelsArr = channels
.filter((x) => x.channel_type !== "SavedMessages")
.map((x) => mapChannelWithUnread(x, props.unreads));
const users = useUsers(
(
channelsArr as (
| Channels.DirectMessageChannel
| Channels.GroupChannel
)[]
).reduce((prev: any, cur) => [...prev, ...cur.recipients], []),
ctx,
);
const users = useUsers(
(
channelsArr as (
| Channels.DirectMessageChannel
| Channels.GroupChannel
)[]
).reduce((prev: any, cur) => [...prev, ...cur.recipients], []),
ctx,
);
channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
return (
<GenericSidebarBase padding>
<UserHeader user={client.user!} />
<ConnectionStatus />
<GenericSidebarList>
{!isTouchscreenDevice && (
<>
<ConditionalLink active={pathname === "/"} to="/">
<ButtonItem active={pathname === "/"}>
<Home size={20} />
<span>
<Text id="app.navigation.tabs.home" />
</span>
</ButtonItem>
</ConditionalLink>
<ConditionalLink
active={pathname === "/friends"}
to="/friends">
<ButtonItem
active={pathname === "/friends"}
alert={
typeof users.find(
(user) =>
user?.relationship ===
UsersNS.Relationship.Incoming,
) !== "undefined"
? "unread"
: undefined
}>
<UserDetail size={20} />
<span>
<Text id="app.navigation.tabs.friends" />
</span>
</ButtonItem>
</ConditionalLink>
</>
)}
<ConditionalLink
active={obj?.channel_type === "SavedMessages"}
to="/open/saved">
<ButtonItem active={obj?.channel_type === "SavedMessages"}>
<Notepad size={20} />
<span>
<Text id="app.navigation.tabs.saved" />
</span>
</ButtonItem>
</ConditionalLink>
{import.meta.env.DEV && (
<Link to="/dev">
<ButtonItem active={pathname === "/dev"}>
<Wrench size={20} />
<span>
<Text id="app.navigation.tabs.dev" />
</span>
</ButtonItem>
</Link>
)}
<Category
text={<Text id="app.main.categories.conversations" />}
action={() =>
openScreen({
id: "special_input",
type: "create_group",
})
}
/>
{channelsArr.length === 0 && <img src={placeholderSVG} />}
{channelsArr.map((x) => {
let user;
if (x.channel_type === "DirectMessage") {
if (!x.active) return null;
return (
<GenericSidebarBase padding>
<UserHeader user={client.user!} />
<ConnectionStatus />
<GenericSidebarList>
{!isTouchscreenDevice && (
<>
<ConditionalLink active={pathname === "/"} to="/">
<ButtonItem active={pathname === "/"}>
<Home size={20} />
<span>
<Text id="app.navigation.tabs.home" />
</span>
</ButtonItem>
</ConditionalLink>
<ConditionalLink
active={pathname === "/friends"}
to="/friends">
<ButtonItem
active={pathname === "/friends"}
alert={
typeof users.find(
(user) =>
user?.relationship ===
UsersNS.Relationship.Incoming,
) !== "undefined"
? "unread"
: undefined
}>
<UserDetail size={20} />
<span>
<Text id="app.navigation.tabs.friends" />
</span>
</ButtonItem>
</ConditionalLink>
</>
)}
<ConditionalLink
active={obj?.channel_type === "SavedMessages"}
to="/open/saved">
<ButtonItem active={obj?.channel_type === "SavedMessages"}>
<Notepad size={20} />
<span>
<Text id="app.navigation.tabs.saved" />
</span>
</ButtonItem>
</ConditionalLink>
{import.meta.env.DEV && (
<Link to="/dev">
<ButtonItem active={pathname === "/dev"}>
<Wrench size={20} />
<span>
<Text id="app.navigation.tabs.dev" />
</span>
</ButtonItem>
</Link>
)}
<Category
text={<Text id="app.main.categories.conversations" />}
action={() =>
openScreen({
id: "special_input",
type: "create_group",
})
}
/>
{channelsArr.length === 0 && <img src={placeholderSVG} />}
{channelsArr.map((x) => {
let user;
if (x.channel_type === "DirectMessage") {
if (!x.active) return null;
let recipient = client.channels.getRecipient(x._id);
user = users.find((x) => x?._id === recipient);
let recipient = client.channels.getRecipient(x._id);
user = users.find((x) => x?._id === recipient);
if (!user) {
console.warn(
`Skipped DM ${x._id} because user was missing.`,
);
return null;
}
}
if (!user) {
console.warn(
`Skipped DM ${x._id} because user was missing.`,
);
return null;
}
}
return (
<ConditionalLink
active={x._id === channel}
to={`/channel/${x._id}`}>
<ChannelButton
user={user}
channel={x}
alert={x.unread}
alertCount={x.alertCount}
active={x._id === channel}
/>
</ConditionalLink>
);
})}
<PaintCounter />
</GenericSidebarList>
</GenericSidebarBase>
);
return (
<ConditionalLink
active={x._id === channel}
to={`/channel/${x._id}`}>
<ChannelButton
user={user}
channel={x}
alert={x.unread}
alertCount={x.alertCount}
active={x._id === channel}
/>
</ConditionalLink>
);
})}
<PaintCounter />
</GenericSidebarList>
</GenericSidebarBase>
);
}
export default connectState(
HomeSidebar,
(state) => {
return {
unreads: state.unreads,
};
},
true,
HomeSidebar,
(state) => {
return {
unreads: state.unreads,
};
},
true,
);

View File

@@ -15,10 +15,10 @@ import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import {
useChannels,
useForceUpdate,
useSelf,
useServers,
useChannels,
useForceUpdate,
useSelf,
useServers,
} from "../../../context/revoltjs/hooks";
import ServerIcon from "../../common/ServerIcon";
@@ -31,268 +31,268 @@ import { mapChannelWithUnread } from "./common";
import { Children } from "../../../types/Preact";
function Icon({
children,
unread,
size,
children,
unread,
size,
}: {
children: Children;
unread?: "mention" | "unread";
size: number;
children: Children;
unread?: "mention" | "unread";
size: number;
}) {
return (
<svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32">
<use href="#serverIndicator" />
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={unread ? "url(#server)" : undefined}>
{children}
</foreignObject>
{unread === "unread" && (
<circle cx="27" cy="5" r="5" fill={"white"} />
)}
{unread === "mention" && (
<circle cx="27" cy="5" r="5" fill={"red"} />
)}
</svg>
);
return (
<svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32">
<use href="#serverIndicator" />
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={unread ? "url(#server)" : undefined}>
{children}
</foreignObject>
{unread === "unread" && (
<circle cx="27" cy="5" r="5" fill={"white"} />
)}
{unread === "mention" && (
<circle cx="27" cy="5" r="5" fill={"red"} />
)}
</svg>
);
}
const ServersBase = styled.div`
width: 56px;
height: 100%;
display: flex;
flex-direction: column;
width: 56px;
height: 100%;
display: flex;
flex-direction: column;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`;
const ServerList = styled.div`
flex-grow: 1;
display: flex;
overflow-y: scroll;
padding-bottom: 48px;
flex-direction: column;
// border-right: 2px solid var(--accent);
flex-grow: 1;
display: flex;
overflow-y: scroll;
padding-bottom: 48px;
flex-direction: column;
// border-right: 2px solid var(--accent);
scrollbar-width: none;
scrollbar-width: none;
> :first-child > svg {
margin: 6px 0 6px 4px;
}
> :first-child > svg {
margin: 6px 0 6px 4px;
}
&::-webkit-scrollbar {
width: 0px;
}
&::-webkit-scrollbar {
width: 0px;
}
`;
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
height: 58px;
display: flex;
align-items: center;
justify-content: flex-end;
height: 58px;
display: flex;
align-items: center;
justify-content: flex-end;
> * {
// outline: 1px solid red;
}
> * {
// outline: 1px solid red;
}
> div {
width: 46px;
height: 46px;
display: grid;
place-items: center;
> div {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-start-start-radius: 50%;
border-end-start-radius: 50%;
border-start-start-radius: 50%;
border-end-start-radius: 50%;
&:active {
transform: translateY(1px);
}
&:active {
transform: translateY(1px);
}
${(props) =>
props.active &&
css`
background: var(--sidebar-active);
&:active {
transform: none;
}
`}
}
${(props) =>
props.active &&
css`
background: var(--sidebar-active);
&:active {
transform: none;
}
`}
}
span {
width: 6px;
height: 46px;
span {
width: 6px;
height: 46px;
${(props) =>
props.active &&
css`
background-color: var(--sidebar-active);
${(props) =>
props.active &&
css`
background-color: var(--sidebar-active);
&::before,
&::after {
// outline: 1px solid blue;
}
&::before,
&::after {
// outline: 1px solid blue;
}
&::before,
&::after {
content: "";
display: block;
position: relative;
&::before,
&::after {
content: "";
display: block;
position: relative;
width: 31px;
height: 72px;
margin-top: -72px;
margin-left: -25px;
z-index: -1;
width: 31px;
height: 72px;
margin-top: -72px;
margin-left: -25px;
z-index: -1;
background-color: var(--background);
border-bottom-right-radius: 32px;
background-color: var(--background);
border-bottom-right-radius: 32px;
box-shadow: 0 32px 0 0 var(--sidebar-active);
}
box-shadow: 0 32px 0 0 var(--sidebar-active);
}
&::after {
transform: scaleY(-1) translateY(-118px);
}
`}
}
&::after {
transform: scaleY(-1) translateY(-118px);
}
`}
}
${(props) =>
(!props.active || props.home) &&
css`
cursor: pointer;
`}
${(props) =>
(!props.active || props.home) &&
css`
cursor: pointer;
`}
`;
interface Props {
unreads: Unreads;
lastOpened: LastOpened;
unreads: Unreads;
lastOpened: LastOpened;
}
export function ServerListSidebar({ unreads, lastOpened }: Props) {
const ctx = useForceUpdate();
const self = useSelf(ctx);
const activeServers = useServers(undefined, ctx) as Servers.Server[];
const channels = (useChannels(undefined, ctx) as Channel[]).map((x) =>
mapChannelWithUnread(x, unreads),
);
const ctx = useForceUpdate();
const self = useSelf(ctx);
const activeServers = useServers(undefined, ctx) as Servers.Server[];
const channels = (useChannels(undefined, ctx) as Channel[]).map((x) =>
mapChannelWithUnread(x, unreads),
);
const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id);
const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id);
const servers = activeServers.map((server) => {
let alertCount = 0;
for (let id of server.channels) {
let channel = channels.find((x) => x._id === id);
if (channel?.alertCount) {
alertCount += channel.alertCount;
}
}
const servers = activeServers.map((server) => {
let alertCount = 0;
for (let id of server.channels) {
let channel = channels.find((x) => x._id === id);
if (channel?.alertCount) {
alertCount += channel.alertCount;
}
}
return {
...server,
unread: (typeof server.channels.find((x) =>
unreadChannels.includes(x),
) !== "undefined"
? alertCount > 0
? "mention"
: "unread"
: undefined) as "mention" | "unread" | undefined,
alertCount,
};
});
return {
...server,
unread: (typeof server.channels.find((x) =>
unreadChannels.includes(x),
) !== "undefined"
? alertCount > 0
? "mention"
: "unread"
: undefined) as "mention" | "unread" | undefined,
alertCount,
};
});
const path = useLocation().pathname;
const { server: server_id } = useParams<{ server?: string }>();
const server = servers.find((x) => x!._id == server_id);
const path = useLocation().pathname;
const { server: server_id } = useParams<{ server?: string }>();
const server = servers.find((x) => x!._id == server_id);
const { openScreen } = useIntermediate();
const { openScreen } = useIntermediate();
let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0;
for (let x of channels) {
if (
((x.channel_type === "DirectMessage" && x.active) ||
x.channel_type === "Group") &&
x.unread
) {
homeUnread = "unread";
alertCount += x.alertCount ?? 0;
}
}
let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0;
for (let x of channels) {
if (
((x.channel_type === "DirectMessage" && x.active) ||
x.channel_type === "Group") &&
x.unread
) {
homeUnread = "unread";
alertCount += x.alertCount ?? 0;
}
}
if (alertCount > 0) homeUnread = "mention";
const homeActive =
typeof server === "undefined" && !path.startsWith("/invite");
if (alertCount > 0) homeUnread = "mention";
const homeActive =
typeof server === "undefined" && !path.startsWith("/invite");
return (
<ServersBase>
<ServerList>
<ConditionalLink
active={homeActive}
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
<ServerEntry home active={homeActive}>
<div
onContextMenu={attachContextMenu("Status")}
onClick={() =>
homeActive && openContextMenu("Status")
}>
<Icon size={42} unread={homeUnread}>
<UserIcon target={self} size={32} status />
</Icon>
</div>
<span />
</ServerEntry>
</ConditionalLink>
<LineDivider />
{servers.map((entry) => {
const active = entry!._id === server?._id;
const id = lastOpened[entry!._id];
return (
<ServersBase>
<ServerList>
<ConditionalLink
active={homeActive}
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
<ServerEntry home active={homeActive}>
<div
onContextMenu={attachContextMenu("Status")}
onClick={() =>
homeActive && openContextMenu("Status")
}>
<Icon size={42} unread={homeUnread}>
<UserIcon target={self} size={32} status />
</Icon>
</div>
<span />
</ServerEntry>
</ConditionalLink>
<LineDivider />
{servers.map((entry) => {
const active = entry!._id === server?._id;
const id = lastOpened[entry!._id];
return (
<ConditionalLink
active={active}
to={
`/server/${entry!._id}` +
(id ? `/channel/${id}` : "")
}>
<ServerEntry
active={active}
onContextMenu={attachContextMenu("Menu", {
server: entry!._id,
})}>
<Tooltip content={entry.name} placement="right">
<Icon size={42} unread={entry.unread}>
<ServerIcon size={32} target={entry} />
</Icon>
</Tooltip>
<span />
</ServerEntry>
</ConditionalLink>
);
})}
<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_server",
})
}>
<Plus size={36} />
</IconButton>
<PaintCounter small />
</ServerList>
</ServersBase>
);
return (
<ConditionalLink
active={active}
to={
`/server/${entry!._id}` +
(id ? `/channel/${id}` : "")
}>
<ServerEntry
active={active}
onContextMenu={attachContextMenu("Menu", {
server: entry!._id,
})}>
<Tooltip content={entry.name} placement="right">
<Icon size={42} unread={entry.unread}>
<ServerIcon size={32} target={entry} />
</Icon>
</Tooltip>
<span />
</ServerEntry>
</ConditionalLink>
);
})}
<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_server",
})
}>
<Plus size={36} />
</IconButton>
<PaintCounter small />
</ServerList>
</ServersBase>
);
}
export default connectState(ServerListSidebar, (state) => {
return {
unreads: state.unreads,
lastOpened: state.lastOpened,
};
return {
unreads: state.unreads,
lastOpened: state.lastOpened,
};
});

View File

@@ -13,9 +13,9 @@ import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads";
import {
useChannels,
useForceUpdate,
useServer,
useChannels,
useForceUpdate,
useServer,
} from "../../../context/revoltjs/hooks";
import CollapsibleSection from "../../common/CollapsibleSection";
@@ -27,124 +27,124 @@ import { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus";
interface Props {
unreads: Unreads;
unreads: Unreads;
}
const ServerBase = styled.div`
height: 100%;
width: 240px;
display: flex;
flex-shrink: 0;
flex-direction: column;
background: var(--secondary-background);
height: 100%;
width: 240px;
display: flex;
flex-shrink: 0;
flex-direction: column;
background: var(--secondary-background);
border-start-start-radius: 8px;
border-end-start-radius: 8px;
overflow: hidden;
border-start-start-radius: 8px;
border-end-start-radius: 8px;
overflow: hidden;
`;
const ServerList = styled.div`
padding: 6px;
flex-grow: 1;
overflow-y: scroll;
padding: 6px;
flex-grow: 1;
overflow-y: scroll;
> svg {
width: 100%;
}
> svg {
width: 100%;
}
`;
function ServerSidebar(props: Props) {
const { server: server_id, channel: channel_id } =
useParams<{ server?: string; channel?: string }>();
const ctx = useForceUpdate();
const { server: server_id, channel: channel_id } =
useParams<{ server?: string; channel?: string }>();
const ctx = useForceUpdate();
const server = useServer(server_id, ctx);
if (!server) return <Redirect to="/" />;
const server = useServer(server_id, ctx);
if (!server) return <Redirect to="/" />;
const channels = (
useChannels(server.channels, ctx).filter(
(entry) => typeof entry !== "undefined",
) as Readonly<Channels.TextChannel | Channels.VoiceChannel>[]
).map((x) => mapChannelWithUnread(x, props.unreads));
const channels = (
useChannels(server.channels, ctx).filter(
(entry) => typeof entry !== "undefined",
) as Readonly<Channels.TextChannel | Channels.VoiceChannel>[]
).map((x) => mapChannelWithUnread(x, props.unreads));
const channel = channels.find((x) => x?._id === channel_id);
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel }, ctx);
const channel = channels.find((x) => x?._id === channel_id);
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel }, ctx);
useEffect(() => {
if (!channel_id) return;
useEffect(() => {
if (!channel_id) return;
dispatch({
type: "LAST_OPENED_SET",
parent: server_id!,
child: channel_id!,
});
}, [channel_id]);
dispatch({
type: "LAST_OPENED_SET",
parent: server_id!,
child: channel_id!,
});
}, [channel_id]);
let uncategorised = new Set(server.channels);
let elements = [];
let uncategorised = new Set(server.channels);
let elements = [];
function addChannel(id: string) {
const entry = channels.find((x) => x._id === id);
if (!entry) return;
function addChannel(id: string) {
const entry = channels.find((x) => x._id === id);
if (!entry) return;
const active = channel?._id === entry._id;
const active = channel?._id === entry._id;
return (
<ConditionalLink
key={entry._id}
active={active}
to={`/server/${server!._id}/channel/${entry._id}`}>
<ChannelButton
channel={entry}
active={active}
alert={entry.unread}
compact
/>
</ConditionalLink>
);
}
return (
<ConditionalLink
key={entry._id}
active={active}
to={`/server/${server!._id}/channel/${entry._id}`}>
<ChannelButton
channel={entry}
active={active}
alert={entry.unread}
compact
/>
</ConditionalLink>
);
}
if (server.categories) {
for (let category of server.categories) {
let channels = [];
for (let id of category.channels) {
uncategorised.delete(id);
channels.push(addChannel(id));
}
if (server.categories) {
for (let category of server.categories) {
let channels = [];
for (let id of category.channels) {
uncategorised.delete(id);
channels.push(addChannel(id));
}
elements.push(
<CollapsibleSection
id={`category_${category.id}`}
defaultValue
summary={<Category text={category.title} />}>
{channels}
</CollapsibleSection>,
);
}
}
elements.push(
<CollapsibleSection
id={`category_${category.id}`}
defaultValue
summary={<Category text={category.title} />}>
{channels}
</CollapsibleSection>,
);
}
}
for (let id of Array.from(uncategorised).reverse()) {
elements.unshift(addChannel(id));
}
for (let id of Array.from(uncategorised).reverse()) {
elements.unshift(addChannel(id));
}
return (
<ServerBase>
<ServerHeader server={server} ctx={ctx} />
<ConnectionStatus />
<ServerList
onContextMenu={attachContextMenu("Menu", {
server_list: server._id,
})}>
{elements}
</ServerList>
<PaintCounter small />
</ServerBase>
);
return (
<ServerBase>
<ServerHeader server={server} ctx={ctx} />
<ConnectionStatus />
<ServerList
onContextMenu={attachContextMenu("Menu", {
server_list: server._id,
})}>
{elements}
</ServerList>
<PaintCounter small />
</ServerBase>
);
}
export default connectState(ServerSidebar, (state) => {
return {
unreads: state.unreads,
};
return {
unreads: state.unreads,
};
});

View File

@@ -8,96 +8,96 @@ import { Unreads } from "../../../redux/reducers/unreads";
import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
type UnreadProps = {
channel: Channel;
unreads: Unreads;
channel: Channel;
unreads: Unreads;
};
export function useUnreads(
{ channel, unreads }: UnreadProps,
context?: HookContext,
{ channel, unreads }: UnreadProps,
context?: HookContext,
) {
const ctx = useForceUpdate(context);
const ctx = useForceUpdate(context);
useLayoutEffect(() => {
function checkUnread(target?: Channel) {
if (!target) return;
if (target._id !== channel._id) return;
if (
target.channel_type === "SavedMessages" ||
target.channel_type === "VoiceChannel"
)
return;
useLayoutEffect(() => {
function checkUnread(target?: Channel) {
if (!target) return;
if (target._id !== channel._id) return;
if (
target.channel_type === "SavedMessages" ||
target.channel_type === "VoiceChannel"
)
return;
const unread = unreads[channel._id]?.last_id;
if (target.last_message) {
const message =
typeof target.last_message === "string"
? target.last_message
: target.last_message._id;
if (!unread || (unread && message.localeCompare(unread) > 0)) {
dispatch({
type: "UNREADS_MARK_READ",
channel: channel._id,
message,
});
const unread = unreads[channel._id]?.last_id;
if (target.last_message) {
const message =
typeof target.last_message === "string"
? target.last_message
: target.last_message._id;
if (!unread || (unread && message.localeCompare(unread) > 0)) {
dispatch({
type: "UNREADS_MARK_READ",
channel: channel._id,
message,
});
ctx.client.req(
"PUT",
`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
);
}
}
}
ctx.client.req(
"PUT",
`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
);
}
}
}
checkUnread(channel);
checkUnread(channel);
ctx.client.channels.addListener("mutation", checkUnread);
return () =>
ctx.client.channels.removeListener("mutation", checkUnread);
}, [channel, unreads]);
ctx.client.channels.addListener("mutation", checkUnread);
return () =>
ctx.client.channels.removeListener("mutation", checkUnread);
}, [channel, unreads]);
}
export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
let last_message_id;
if (
channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group"
) {
last_message_id = channel.last_message?._id;
} else if (channel.channel_type === "TextChannel") {
last_message_id = channel.last_message;
} else {
return {
...channel,
unread: undefined,
alertCount: undefined,
timestamp: channel._id,
};
}
let last_message_id;
if (
channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group"
) {
last_message_id = channel.last_message?._id;
} else if (channel.channel_type === "TextChannel") {
last_message_id = channel.last_message;
} else {
return {
...channel,
unread: undefined,
alertCount: undefined,
timestamp: channel._id,
};
}
let unread: "mention" | "unread" | undefined;
let alertCount: undefined | number;
if (last_message_id && unreads) {
const u = unreads[channel._id];
if (u) {
if (u.mentions && u.mentions.length > 0) {
alertCount = u.mentions.length;
unread = "mention";
} else if (
u.last_id &&
last_message_id.localeCompare(u.last_id) > 0
) {
unread = "unread";
}
} else {
unread = "unread";
}
}
let unread: "mention" | "unread" | undefined;
let alertCount: undefined | number;
if (last_message_id && unreads) {
const u = unreads[channel._id];
if (u) {
if (u.mentions && u.mentions.length > 0) {
alertCount = u.mentions.length;
unread = "mention";
} else if (
u.last_id &&
last_message_id.localeCompare(u.last_id) > 0
) {
unread = "unread";
}
} else {
unread = "unread";
}
}
return {
...channel,
timestamp: last_message_id ?? channel._id,
unread,
alertCount,
};
return {
...channel,
timestamp: last_message_id ?? channel._id,
unread,
alertCount,
};
}

View File

@@ -1,40 +1,40 @@
import { useRenderState } from "../../../lib/renderer/Singleton";
interface Props {
id: string;
id: string;
}
export function ChannelDebugInfo({ id }: Props) {
if (process.env.NODE_ENV !== "development") return null;
let view = useRenderState(id);
if (!view) return null;
if (process.env.NODE_ENV !== "development") return null;
let view = useRenderState(id);
if (!view) return null;
return (
<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
<span
style={{
display: "block",
fontSize: "12px",
textTransform: "uppercase",
fontWeight: "600",
}}>
Channel Info
</span>
<p style={{ fontSize: "10px", userSelect: "text" }}>
State: <b>{view.type}</b> <br />
{view.type === "RENDER" && view.messages.length > 0 && (
<>
Start: <b>{view.messages[0]._id}</b> <br />
End:{" "}
<b>
{view.messages[view.messages.length - 1]._id}
</b>{" "}
<br />
At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
</>
)}
</p>
</span>
);
return (
<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
<span
style={{
display: "block",
fontSize: "12px",
textTransform: "uppercase",
fontWeight: "600",
}}>
Channel Info
</span>
<p style={{ fontSize: "10px", userSelect: "text" }}>
State: <b>{view.type}</b> <br />
{view.type === "RENDER" && view.messages.length > 0 && (
<>
Start: <b>{view.messages[0]._id}</b> <br />
End:{" "}
<b>
{view.messages[view.messages.length - 1]._id}
</b>{" "}
<br />
At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
</>
)}
</p>
</span>
);
}

View File

@@ -8,15 +8,15 @@ import { useContext, useEffect, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import {
AppContext,
ClientStatus,
StatusContext,
AppContext,
ClientStatus,
StatusContext,
} from "../../../context/revoltjs/RevoltClient";
import {
HookContext,
useChannel,
useForceUpdate,
useUsers,
HookContext,
useChannel,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import Category from "../../ui/Category";
@@ -28,35 +28,35 @@ import { UserButton } from "../items/ButtonItem";
import { ChannelDebugInfo } from "./ChannelDebugInfo";
interface Props {
ctx: HookContext;
ctx: HookContext;
}
export default function MemberSidebar(props: { channel?: Channels.Channel }) {
const ctx = useForceUpdate();
const { channel: cid } = useParams<{ channel: string }>();
const channel = props.channel ?? useChannel(cid, ctx);
const ctx = useForceUpdate();
const { channel: cid } = useParams<{ channel: string }>();
const channel = props.channel ?? useChannel(cid, ctx);
switch (channel?.channel_type) {
case "Group":
return <GroupMemberSidebar channel={channel} ctx={ctx} />;
case "TextChannel":
return <ServerMemberSidebar channel={channel} ctx={ctx} />;
default:
return null;
}
switch (channel?.channel_type) {
case "Group":
return <GroupMemberSidebar channel={channel} ctx={ctx} />;
case "TextChannel":
return <ServerMemberSidebar channel={channel} ctx={ctx} />;
default:
return null;
}
}
export function GroupMemberSidebar({
channel,
ctx,
channel,
ctx,
}: Props & { channel: Channels.GroupChannel }) {
const { openScreen } = useIntermediate();
const users = useUsers(undefined, ctx);
let members = channel.recipients
.map((x) => users.find((y) => y?._id === x))
.filter((x) => typeof x !== "undefined") as User[];
const { openScreen } = useIntermediate();
const users = useUsers(undefined, ctx);
let members = channel.recipients
.map((x) => users.find((y) => y?._id === x))
.filter((x) => typeof x !== "undefined") as User[];
/*const voice = useContext(VoiceContext);
/*const voice = useContext(VoiceContext);
const voiceActive = voice.roomId === channel._id;
let voiceParticipants: User[] = [];
@@ -71,32 +71,32 @@ export function GroupMemberSidebar({
voiceParticipants.sort((a, b) => a.username.localeCompare(b.username));
}*/
members.sort((a, b) => {
// ! FIXME: should probably rewrite all this code
let l =
+(
(a.online && a.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let r =
+(
(b.online && b.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
members.sort((a, b) => {
// ! FIXME: should probably rewrite all this code
let l =
+(
(a.online && a.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let r =
+(
(b.online && b.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let n = r - l;
if (n !== 0) {
return n;
}
let n = r - l;
if (n !== 0) {
return n;
}
return a.username.localeCompare(b.username);
});
return a.username.localeCompare(b.username);
});
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
{/*voiceActive && voiceParticipants.length !== 0 && (
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
{/*voiceActive && voiceParticipants.length !== 0 && (
<Fragment>
<Category
type="members"
@@ -121,146 +121,146 @@ export function GroupMemberSidebar({
)}
</Fragment>
)*/}
{!((members.length === 0) /*&& voiceActive*/) && (
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" /> {" "}
{channel.recipients.length}
</span>
}
/>
)}
{members.length === 0 && (
/*!voiceActive &&*/ <img src={placeholderSVG} />
)}
{members.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</GenericSidebarList>
</GenericSidebarBase>
);
{!((members.length === 0) /*&& voiceActive*/) && (
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" /> {" "}
{channel.recipients.length}
</span>
}
/>
)}
{members.length === 0 && (
/*!voiceActive &&*/ <img src={placeholderSVG} />
)}
{members.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</GenericSidebarList>
</GenericSidebarBase>
);
}
export function ServerMemberSidebar({
channel,
ctx,
channel,
ctx,
}: Props & { channel: Channels.TextChannel }) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(
undefined,
);
const users = useUsers(members?.map((x) => x._id.user) ?? []).filter(
(x) => typeof x !== "undefined",
ctx,
) as Users.User[];
const { openScreen } = useIntermediate();
const status = useContext(StatusContext);
const client = useContext(AppContext);
const [members, setMembers] = useState<Servers.Member[] | undefined>(
undefined,
);
const users = useUsers(members?.map((x) => x._id.user) ?? []).filter(
(x) => typeof x !== "undefined",
ctx,
) as Users.User[];
const { openScreen } = useIntermediate();
const status = useContext(StatusContext);
const client = useContext(AppContext);
useEffect(() => {
if (status === ClientStatus.ONLINE && typeof members === "undefined") {
client.servers.members
.fetchMembers(channel.server)
.then((members) => setMembers(members));
}
}, [status]);
useEffect(() => {
if (status === ClientStatus.ONLINE && typeof members === "undefined") {
client.servers.members
.fetchMembers(channel.server)
.then((members) => setMembers(members));
}
}, [status]);
// ! FIXME: temporary code
useEffect(() => {
function onPacket(packet: ClientboundNotification) {
if (!members) return;
if (packet.type === "ServerMemberJoin") {
if (packet.id !== channel.server) return;
setMembers([
...members,
{ _id: { server: packet.id, user: packet.user } },
]);
} else if (packet.type === "ServerMemberLeave") {
if (packet.id !== channel.server) return;
setMembers(
members.filter(
(x) =>
!(
x._id.user === packet.user &&
x._id.server === packet.id
),
),
);
}
}
// ! FIXME: temporary code
useEffect(() => {
function onPacket(packet: ClientboundNotification) {
if (!members) return;
if (packet.type === "ServerMemberJoin") {
if (packet.id !== channel.server) return;
setMembers([
...members,
{ _id: { server: packet.id, user: packet.user } },
]);
} else if (packet.type === "ServerMemberLeave") {
if (packet.id !== channel.server) return;
setMembers(
members.filter(
(x) =>
!(
x._id.user === packet.user &&
x._id.server === packet.id
),
),
);
}
}
client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket);
}, [members]);
client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket);
}, [members]);
// copy paste from above
users.sort((a, b) => {
// ! FIXME: should probably rewrite all this code
let l =
+(
(a.online && a.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let r =
+(
(b.online && b.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
// copy paste from above
users.sort((a, b) => {
// ! FIXME: should probably rewrite all this code
let l =
+(
(a.online && a.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let r =
+(
(b.online && b.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
let n = r - l;
if (n !== 0) {
return n;
}
let n = r - l;
if (n !== 0) {
return n;
}
return a.username.localeCompare(b.username);
});
return a.username.localeCompare(b.username);
});
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" /> {" "}
{users.length}
</span>
}
/>
{!members && <Preloader type="ring" />}
{members && users.length === 0 && <img src={placeholderSVG} />}
{users.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</GenericSidebarList>
</GenericSidebarBase>
);
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" /> {" "}
{users.length}
</span>
}
/>
{!members && <Preloader type="ring" />}
{members && users.length === 0 && <img src={placeholderSVG} />}
{users.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</GenericSidebarList>
</GenericSidebarBase>
);
}