mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Run prettier on all files.
This commit is contained in:
@@ -1,88 +1,96 @@
|
||||
import IconButton from "../ui/IconButton";
|
||||
import UserIcon from "../common/user/UserIcon";
|
||||
import styled, { css } from "styled-components";
|
||||
import { useSelf } from "../../context/revoltjs/hooks";
|
||||
import { useHistory, useLocation } from "react-router";
|
||||
import ConditionalLink from "../../lib/ConditionalLink";
|
||||
import { Message, Group } from "@styled-icons/boxicons-solid";
|
||||
import { LastOpened } from "../../redux/reducers/last_opened";
|
||||
import { useHistory, useLocation } from "react-router";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import ConditionalLink from "../../lib/ConditionalLink";
|
||||
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { LastOpened } from "../../redux/reducers/last_opened";
|
||||
|
||||
import { useSelf } from "../../context/revoltjs/hooks";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState(BottomNavigation, state => {
|
||||
return {
|
||||
lastOpened: state.lastOpened
|
||||
}
|
||||
export default connectState(BottomNavigation, (state) => {
|
||||
return {
|
||||
lastOpened: state.lastOpened,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { Route, Switch } from "react-router";
|
||||
import SidebarBase from "./SidebarBase";
|
||||
|
||||
import SidebarBase from "./SidebarBase";
|
||||
import HomeSidebar from "./left/HomeSidebar";
|
||||
import ServerListSidebar from "./left/ServerListSidebar";
|
||||
import ServerSidebar from "./left/ServerSidebar";
|
||||
import HomeSidebar from "./left/HomeSidebar";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Route, Switch } from "react-router";
|
||||
import SidebarBase from "./SidebarBase";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
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%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,158 +1,223 @@
|
||||
import classNames from 'classnames';
|
||||
import styles from "./Item.module.scss";
|
||||
import Tooltip from '../../common/Tooltip';
|
||||
import IconButton from '../../ui/IconButton';
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { X, Crown } from "@styled-icons/boxicons-regular";
|
||||
import { Children } from "../../../types/Preact";
|
||||
import UserIcon from '../../common/user/UserIcon';
|
||||
import ChannelIcon from '../../common/ChannelIcon';
|
||||
import UserStatus from '../../common/user/UserStatus';
|
||||
import { attachContextMenu } from 'preact-context-menu';
|
||||
import { Channels, Users } from "revolt.js/dist/api/objects";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { useIntermediate } from '../../../context/intermediate/Intermediate';
|
||||
import { stopPropagation } from '../../../lib/stopPropagation';
|
||||
|
||||
type CommonProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
|
||||
active?: boolean
|
||||
alert?: 'unread' | 'mention'
|
||||
alertCount?: number
|
||||
}
|
||||
import styles from "./Item.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import ChannelIcon from "../../common/ChannelIcon";
|
||||
import Tooltip from "../../common/Tooltip";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import UserStatus from "../../common/user/UserStatus";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
|
||||
type CommonProps = Omit<
|
||||
JSX.HTMLAttributes<HTMLDivElement>,
|
||||
"children" | "as"
|
||||
> & {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import Banner from "../../ui/Banner";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,152 +1,193 @@
|
||||
import {
|
||||
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";
|
||||
import { Users as UsersNS } from "revolt.js/dist/api/objects";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { Home, UserDetail, Wrench, Notepad } from "@styled-icons/boxicons-solid";
|
||||
|
||||
import Category from '../../ui/Category';
|
||||
import { dispatch } from "../../../redux";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import UserHeader from "../../common/user/UserHeader";
|
||||
import { Channels } from "revolt.js/dist/api/objects";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import ConnectionStatus from '../items/ConnectionStatus';
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
import { Users as UsersNS } from 'revolt.js/dist/api/objects';
|
||||
import ButtonItem, { ChannelButton } from '../items/ButtonItem';
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import { Link, Redirect, useLocation, useParams } from "react-router-dom";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useDMs, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import {
|
||||
useDMs,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
|
||||
import UserHeader from "../../common/user/UserHeader";
|
||||
import Category from "../../ui/Category";
|
||||
import placeholderSVG from "../items/placeholder.svg";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
||||
import ConnectionStatus from "../items/ConnectionStatus";
|
||||
|
||||
type Props = {
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
||||
function HomeSidebar(props: Props) {
|
||||
const { pathname } = useLocation();
|
||||
const client = useContext(AppContext);
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
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 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
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 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));
|
||||
|
||||
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);
|
||||
|
||||
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>
|
||||
);
|
||||
unreads: Unreads;
|
||||
};
|
||||
|
||||
function HomeSidebar(props: Props) {
|
||||
const { pathname } = useLocation();
|
||||
const client = useContext(AppContext);
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
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 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
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 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));
|
||||
|
||||
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);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState(
|
||||
HomeSidebar,
|
||||
state => {
|
||||
return {
|
||||
unreads: state.unreads
|
||||
};
|
||||
},
|
||||
true
|
||||
HomeSidebar,
|
||||
(state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
};
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -1,257 +1,298 @@
|
||||
import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { Channel, Servers } from "revolt.js/dist/api/objects";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { attachContextMenu, openContextMenu } from "preact-context-menu";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { LastOpened } from "../../../redux/reducers/last_opened";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useSelf,
|
||||
useServers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
|
||||
import ServerIcon from "../../common/ServerIcon";
|
||||
import Tooltip from "../../common/Tooltip";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
import LineDivider from "../../ui/LineDivider";
|
||||
import { mapChannelWithUnread } from "./common";
|
||||
import styled, { css } from "styled-components";
|
||||
import ServerIcon from "../../common/ServerIcon";
|
||||
import { Children } from "../../../types/Preact";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import { Channel, Servers } from "revolt.js/dist/api/objects";
|
||||
import { LastOpened } from "../../../redux/reducers/last_opened";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { attachContextMenu, openContextMenu } from 'preact-context-menu';
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useChannels, useForceUpdate, useSelf, useServers } from "../../../context/revoltjs/hooks";
|
||||
|
||||
function Icon({ children, unread, size }: { 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>
|
||||
)
|
||||
import { Children } from "../../../types/Preact";
|
||||
|
||||
function Icon({
|
||||
children,
|
||||
unread,
|
||||
size,
|
||||
}: {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
|
||||
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;
|
||||
|
||||
border-start-start-radius: 50%;
|
||||
border-end-start-radius: 50%;
|
||||
> div {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
border-start-start-radius: 50%;
|
||||
border-end-start-radius: 50%;
|
||||
|
||||
${ props => props.active && css`
|
||||
background: var(--sidebar-active);
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
` }
|
||||
}
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
span {
|
||||
width: 6px;
|
||||
height: 46px;
|
||||
${(props) =>
|
||||
props.active &&
|
||||
css`
|
||||
background: var(--sidebar-active);
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
${ props => props.active && css`
|
||||
background-color: var(--sidebar-active);
|
||||
span {
|
||||
width: 6px;
|
||||
height: 46px;
|
||||
|
||||
&::before, &::after {
|
||||
// outline: 1px solid blue;
|
||||
}
|
||||
${(props) =>
|
||||
props.active &&
|
||||
css`
|
||||
background-color: var(--sidebar-active);
|
||||
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
&::before,
|
||||
&::after {
|
||||
// outline: 1px solid blue;
|
||||
}
|
||||
|
||||
width: 31px;
|
||||
height: 72px;
|
||||
margin-top: -72px;
|
||||
margin-left: -25px;
|
||||
z-index: -1;
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
background-color: var(--background);
|
||||
border-bottom-right-radius: 32px;
|
||||
width: 31px;
|
||||
height: 72px;
|
||||
margin-top: -72px;
|
||||
margin-left: -25px;
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0 32px 0 0 var(--sidebar-active);
|
||||
}
|
||||
background-color: var(--background);
|
||||
border-bottom-right-radius: 32px;
|
||||
|
||||
&::after {
|
||||
transform: scaleY(-1) translateY(-118px);
|
||||
}
|
||||
` }
|
||||
}
|
||||
box-shadow: 0 32px 0 0 var(--sidebar-active);
|
||||
}
|
||||
|
||||
${ props => (!props.active || props.home) && css`
|
||||
cursor: pointer;
|
||||
` }
|
||||
&::after {
|
||||
transform: scaleY(-1) translateY(-118px);
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
${(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 unreadChannels = channels.filter(x => x.unread)
|
||||
.map(x => x._id);
|
||||
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 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 unreadChannels = channels.filter((x) => x.unread).map((x) => x._id);
|
||||
|
||||
return {
|
||||
...server,
|
||||
unread: (typeof server.channels.find(x => unreadChannels.includes(x)) !== 'undefined' ?
|
||||
( alertCount > 0 ? 'mention' : 'unread' ) : undefined) as 'mention' | 'unread' | undefined,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const path = useLocation().pathname;
|
||||
const { server: server_id } = useParams<{ server?: string }>();
|
||||
const server = servers.find(x => x!._id == server_id);
|
||||
return {
|
||||
...server,
|
||||
unread: (typeof server.channels.find((x) =>
|
||||
unreadChannels.includes(x),
|
||||
) !== "undefined"
|
||||
? alertCount > 0
|
||||
? "mention"
|
||||
: "unread"
|
||||
: undefined) as "mention" | "unread" | undefined,
|
||||
alertCount,
|
||||
};
|
||||
});
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
const path = useLocation().pathname;
|
||||
const { server: server_id } = useParams<{ server?: string }>();
|
||||
const server = servers.find((x) => x!._id == server_id);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
if (alertCount > 0) homeUnread = 'mention';
|
||||
const homeActive = typeof server === 'undefined' && !path.startsWith('/invite');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
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 (
|
||||
<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
|
||||
};
|
||||
}
|
||||
);
|
||||
export default connectState(ServerListSidebar, (state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
lastOpened: state.lastOpened,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,134 +1,150 @@
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import { ChannelButton } from "../items/ButtonItem";
|
||||
import { Channels } from "revolt.js/dist/api/objects";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
import { useChannels, useForceUpdate, useServer } from "../../../context/revoltjs/hooks";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
import ConnectionStatus from '../items/ConnectionStatus';
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import styled from "styled-components";
|
||||
import { attachContextMenu } from 'preact-context-menu';
|
||||
import ServerHeader from "../../common/ServerHeader";
|
||||
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import Category from "../../ui/Category";
|
||||
import { dispatch } from "../../../redux";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useServer,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
|
||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||
import ServerHeader from "../../common/ServerHeader";
|
||||
import Category from "../../ui/Category";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
|
||||
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 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 channels = (
|
||||
useChannels(server.channels, ctx).filter(
|
||||
(entry) => typeof entry !== "undefined",
|
||||
) as Readonly<Channels.TextChannel | Channels.VoiceChannel>[]
|
||||
).map((x) => mapChannelWithUnread(x, props.unreads));
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel_id) return;
|
||||
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);
|
||||
|
||||
dispatch({
|
||||
type: 'LAST_OPENED_SET',
|
||||
parent: server_id!,
|
||||
child: channel_id!
|
||||
});
|
||||
}, [ channel_id ]);
|
||||
useEffect(() => {
|
||||
if (!channel_id) return;
|
||||
|
||||
let uncategorised = new Set(server.channels);
|
||||
let elements = [];
|
||||
|
||||
function addChannel(id: string) {
|
||||
const entry = channels.find(x => x._id === id);
|
||||
if (!entry) return;
|
||||
dispatch({
|
||||
type: "LAST_OPENED_SET",
|
||||
parent: server_id!,
|
||||
child: channel_id!,
|
||||
});
|
||||
}, [channel_id]);
|
||||
|
||||
const active = channel?._id === entry._id;
|
||||
let uncategorised = new Set(server.channels);
|
||||
let elements = [];
|
||||
|
||||
return (
|
||||
<ConditionalLink key={entry._id} active={active} to={`/server/${server!._id}/channel/${entry._id}`}>
|
||||
<ChannelButton
|
||||
channel={entry}
|
||||
active={active}
|
||||
alert={entry.unread}
|
||||
compact
|
||||
/>
|
||||
</ConditionalLink>
|
||||
);
|
||||
}
|
||||
function addChannel(id: string) {
|
||||
const entry = channels.find((x) => x._id === id);
|
||||
if (!entry) return;
|
||||
|
||||
if (server.categories) {
|
||||
for (let category of server.categories) {
|
||||
let channels = [];
|
||||
for (let id of category.channels) {
|
||||
uncategorised.delete(id);
|
||||
channels.push(addChannel(id));
|
||||
}
|
||||
const active = channel?._id === entry._id;
|
||||
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category text={category.title} />}>
|
||||
{ channels }
|
||||
</CollapsibleSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ConditionalLink
|
||||
key={entry._id}
|
||||
active={active}
|
||||
to={`/server/${server!._id}/channel/${entry._id}`}>
|
||||
<ChannelButton
|
||||
channel={entry}
|
||||
active={active}
|
||||
alert={entry.unread}
|
||||
compact
|
||||
/>
|
||||
</ConditionalLink>
|
||||
);
|
||||
}
|
||||
|
||||
for (let id of Array.from(uncategorised).reverse()) {
|
||||
elements.unshift(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));
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerBase>
|
||||
<ServerHeader server={server} ctx={ctx} />
|
||||
<ConnectionStatus />
|
||||
<ServerList onContextMenu={attachContextMenu('Menu', { server_list: server._id })}>
|
||||
{ elements }
|
||||
</ServerList>
|
||||
<PaintCounter small />
|
||||
</ServerBase>
|
||||
)
|
||||
};
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category text={category.title} />}>
|
||||
{channels}
|
||||
</CollapsibleSection>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connectState(
|
||||
ServerSidebar,
|
||||
state => {
|
||||
return {
|
||||
unreads: state.unreads
|
||||
};
|
||||
}
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState(ServerSidebar, (state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,76 +1,103 @@
|
||||
import { Channel } from "revolt.js";
|
||||
import { dispatch } from "../../../redux";
|
||||
|
||||
import { useLayoutEffect } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
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) {
|
||||
const ctx = useForceUpdate(context);
|
||||
export function useUnreads(
|
||||
{ channel, unreads }: UnreadProps,
|
||||
context?: HookContext,
|
||||
) {
|
||||
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
|
||||
});
|
||||
|
||||
ctx.client.req('PUT', `/channels/${channel._id}/ack/${message}` as '/channels/id/ack/id');
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
checkUnread(channel);
|
||||
ctx.client.req(
|
||||
"PUT",
|
||||
`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.client.channels.addListener("mutation", checkUnread);
|
||||
return () => ctx.client.channels.removeListener("mutation", checkUnread);
|
||||
}, [channel, unreads]);
|
||||
checkUnread(channel);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,37 +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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,62 @@
|
||||
import { useParams } from "react-router";
|
||||
import { User } from "revolt.js";
|
||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { User } from "revolt.js";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import {
|
||||
HookContext,
|
||||
useChannel,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
|
||||
import Category from "../../ui/Category";
|
||||
import { useParams } from "react-router";
|
||||
import Preloader from "../../ui/Preloader";
|
||||
import placeholderSVG from "../items/placeholder.svg";
|
||||
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import { UserButton } from "../items/ButtonItem";
|
||||
import { ChannelDebugInfo } from "./ChannelDebugInfo";
|
||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
import { AppContext, ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { HookContext, useChannel, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
||||
|
||||
import placeholderSVG from "../items/placeholder.svg";
|
||||
import Preloader from "../../ui/Preloader";
|
||||
|
||||
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 }: 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[];
|
||||
export function GroupMemberSidebar({
|
||||
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 voice = useContext(VoiceContext);
|
||||
/*const voice = useContext(VoiceContext);
|
||||
const voiceActive = voice.roomId === channel._id;
|
||||
|
||||
let voiceParticipants: User[] = [];
|
||||
@@ -54,24 +71,32 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
|
||||
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"
|
||||
@@ -96,104 +121,146 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
|
||||
)}
|
||||
</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 }: 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);
|
||||
export function ServerMemberSidebar({
|
||||
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);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user