Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 924 additions and 521 deletions
import { Wrench } from "@styled-icons/boxicons-solid";
import styled from "styled-components";
import UpdateIndicator from "../common/UpdateIndicator";
const TitlebarBase = styled.div`
height: var(--titlebar-height);
display: flex;
user-select: none;
align-items: center;
.drag {
flex-grow: 1;
-webkit-app-region: drag;
margin-top: 10px;
height: 100%;
}
.quick {
color: var(--secondary-foreground);
> div,
> div > div {
width: var(--titlebar-height) !important;
}
&.disabled {
color: var(--error);
}
&.unavailable {
background: var(--error);
}
}
.title {
-webkit-app-region: drag;
/*height: var(--titlebar-height);*/
font-size: 16px;
font-weight: 600;
margin-inline-start: 10px;
margin-top: 10px;
gap: 6px;
display: flex;
align-items: center;
justify-content: flex-start;
z-index: 90000;
color: var(--titlebar-logo-color);
svg {
margin-bottom: 10px;
}
svg:first-child {
height: calc(var(--titlebar-height) / 3);
}
}
.actions {
z-index: 100;
display: flex;
align-items: center;
margin-inline-start: 6px;
div {
width: calc(
var(--titlebar-height) + var(--titlebar-action-padding)
);
height: var(--titlebar-height);
display: grid;
place-items: center;
transition: 0.2s ease color;
transition: 0.2s ease background-color;
&:hover {
background: var(--primary-background);
}
&.error:hover {
background: var(--error);
}
}
}
`;
export function Titlebar() {
return (
<TitlebarBase>
<div class="title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 193.733 37.438">
<path
d="M23.393,1.382c0,2.787-1.52,4.46-4.764,4.46H13.258V-2.977H18.63C21.873-2.977,23.393-1.254,23.393,1.382Zm-24-11.555,5.2,7.213V25.4h8.666V11.973h2.078l7.4,13.43h9.781l-8.21-14.089A10.355,10.355,0,0,0,32.212,1.027c0-6.183-4.358-11.2-13.075-11.2Zm60.035,0H37.634V25.4H59.426V18.46H46.3v-7.8H57.906V3.966H46.3V-2.969H59.426Zm20.981,26.86-8.818-26.86H62.365L74.984,25.4H85.83L98.449-10.173H89.276Zm56.659-9.173c0-10.693-8.058-18.194-18.194-18.194-10.085,0-18.3,7.5-18.3,18.194a17.9,17.9,0,0,0,18.3,18.244A17.815,17.815,0,0,0,137.066,7.514Zm-27.62,0c0-6.335,3.649-10.338,9.426-10.338,5.676,0,9.376,4,9.376,10.338,0,6.233-3.7,10.338-9.376,10.338C113.095,17.852,109.446,13.747,109.446,7.514ZM141.88-10.173V25.4H161.9v-6.95H150.545V-10.173Zm22.248,7.2h9.426V25.4h8.666V-2.975h9.426v-7.2H164.128Z"
transform="translate(1.586 11.18)"
fill="var(--titlebar-logo-color)"
stroke="var(--titlebar-logo-color)"
stroke-width="1"
/>
</svg>
{window.native.getConfig().build === "dev" && (
<Wrench size="12.5" />
)}
</div>
{/*<div class="actions quick">
<Tooltip
content="Mute"
placement="bottom">
<div onClick={window.native.min}>
<Microphone size={15}/>
</div>
</Tooltip>
<Tooltip
content="Deafen"
placement="bottom">
<div onClick={window.native.min}>
<VolumeFull size={15}/>
</div>
</Tooltip>
</div>*/}
<div class="drag" />
<UpdateIndicator style="titlebar" />
<div class="actions">
<div onClick={window.native.min}>
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
fill="currentColor"
width="10"
height="1"
x="1"
y="6"
/>
</svg>
</div>
<div onClick={window.native.max}>
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
width="9"
height="9"
x="1.5"
y="1.5"
fill="none"
stroke="currentColor"
/>
</svg>
</div>
<div onClick={window.native.close} class="error">
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<polygon
fill="currentColor"
stroke-width="1"
fill-rule="evenodd"
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
style="stroke:currentColor;stroke-width:0.4"
/>
</svg>
</div>
</div>
</TitlebarBase>
);
}
import { Search } from "@styled-icons/boxicons-regular"; import { Message, Group } from "@styled-icons/boxicons-solid";
import { Message, Group, Inbox } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite";
import { useHistory, useLocation } from "react-router"; import { useHistory, useLocation } from "react-router";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
...@@ -8,7 +8,7 @@ import ConditionalLink from "../../lib/ConditionalLink"; ...@@ -8,7 +8,7 @@ import ConditionalLink from "../../lib/ConditionalLink";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { LastOpened } from "../../redux/reducers/last_opened"; import { LastOpened } from "../../redux/reducers/last_opened";
import { useSelf } from "../../context/revoltjs/hooks"; import { useClient } from "../../context/revoltjs/RevoltClient";
import UserIcon from "../common/user/UserIcon"; import UserIcon from "../common/user/UserIcon";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
...@@ -51,8 +51,10 @@ interface Props { ...@@ -51,8 +51,10 @@ interface Props {
lastOpened: LastOpened; lastOpened: LastOpened;
} }
export function BottomNavigation({ lastOpened }: Props) { export const BottomNavigation = observer(({ lastOpened }: Props) => {
const user = useSelf(); const client = useClient();
const user = client.users.get(client.user!._id);
const history = useHistory(); const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
...@@ -114,7 +116,7 @@ export function BottomNavigation({ lastOpened }: Props) { ...@@ -114,7 +116,7 @@ export function BottomNavigation({ lastOpened }: Props) {
</Navbar> </Navbar>
</Base> </Base>
); );
} });
export default connectState(BottomNavigation, (state) => { export default connectState(BottomNavigation, (state) => {
return { return {
......
import { X, Crown } from "@styled-icons/boxicons-regular"; import { X, Crown } from "@styled-icons/boxicons-regular";
import { Channels, Users } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Item.module.scss"; import styles from "./Item.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -14,6 +17,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate"; ...@@ -14,6 +17,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate";
import ChannelIcon from "../../common/ChannelIcon"; import ChannelIcon from "../../common/ChannelIcon";
import Tooltip from "../../common/Tooltip"; import Tooltip from "../../common/Tooltip";
import UserIcon from "../../common/user/UserIcon"; import UserIcon from "../../common/user/UserIcon";
import { Username } from "../../common/user/UserShort";
import UserStatus from "../../common/user/UserStatus"; import UserStatus from "../../common/user/UserStatus";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
...@@ -29,12 +33,12 @@ type CommonProps = Omit< ...@@ -29,12 +33,12 @@ type CommonProps = Omit<
}; };
type UserProps = CommonProps & { type UserProps = CommonProps & {
user: Users.User; user: User;
context?: Channels.Channel; context?: Channel;
channel?: Channels.DirectMessageChannel; channel?: Channel;
}; };
export function UserButton(props: UserProps) { export const UserButton = observer((props: UserProps) => {
const { active, alert, alertCount, user, context, channel, ...divProps } = const { active, alert, alertCount, user, context, channel, ...divProps } =
props; props;
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
...@@ -47,8 +51,7 @@ export function UserButton(props: UserProps) { ...@@ -47,8 +51,7 @@ export function UserButton(props: UserProps) {
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
data-online={ data-online={
typeof channel !== "undefined" || typeof channel !== "undefined" ||
(user.online && (user.online && user.status?.presence !== Presence.Invisible)
user.status?.presence !== Users.Presence.Invisible)
} }
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
user: user._id, user: user._id,
...@@ -63,11 +66,13 @@ export function UserButton(props: UserProps) { ...@@ -63,11 +66,13 @@ export function UserButton(props: UserProps) {
status status
/> />
<div className={styles.name}> <div className={styles.name}>
<div>{user.username}</div> <div>
<Username user={user} />
</div>
{ {
<div className={styles.subText}> <div className={styles.subText}>
{channel?.last_message && alert ? ( {channel?.last_message && alert ? (
channel.last_message.short (channel.last_message as { short: string }).short
) : ( ) : (
<UserStatus user={user} /> <UserStatus user={user} />
)} )}
...@@ -76,7 +81,7 @@ export function UserButton(props: UserProps) { ...@@ -76,7 +81,7 @@ export function UserButton(props: UserProps) {
</div> </div>
<div className={styles.button}> <div className={styles.button}>
{context?.channel_type === "Group" && {context?.channel_type === "Group" &&
context.owner === user._id && ( context.owner_id === user._id && (
<Localizer> <Localizer>
<Tooltip <Tooltip
content={<Text id="app.main.groups.owner" />}> content={<Text id="app.main.groups.owner" />}>
...@@ -106,15 +111,15 @@ export function UserButton(props: UserProps) { ...@@ -106,15 +111,15 @@ export function UserButton(props: UserProps) {
</div> </div>
</div> </div>
); );
} });
type ChannelProps = CommonProps & { type ChannelProps = CommonProps & {
channel: Channels.Channel & { unread?: string }; channel: Channel & { unread?: string };
user?: Users.User; user?: User;
compact?: boolean; compact?: boolean;
}; };
export function ChannelButton(props: ChannelProps) { export const ChannelButton = observer((props: ChannelProps) => {
const { active, alert, alertCount, channel, user, compact, ...divProps } = const { active, alert, alertCount, channel, user, compact, ...divProps } =
props; props;
...@@ -131,7 +136,7 @@ export function ChannelButton(props: ChannelProps) { ...@@ -131,7 +136,7 @@ export function ChannelButton(props: ChannelProps) {
{...divProps} {...divProps}
data-active={active} data-active={active}
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
aria-label={{}} /*FIXME: ADD ARIA LABEL*/ aria-label={channel.name}
className={classNames(styles.item, { [styles.compact]: compact })} className={classNames(styles.item, { [styles.compact]: compact })}
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
channel: channel._id, channel: channel._id,
...@@ -147,12 +152,12 @@ export function ChannelButton(props: ChannelProps) { ...@@ -147,12 +152,12 @@ export function ChannelButton(props: ChannelProps) {
{channel.channel_type === "Group" && ( {channel.channel_type === "Group" && (
<div className={styles.subText}> <div className={styles.subText}>
{channel.last_message && alert ? ( {channel.last_message && alert ? (
channel.last_message.short (channel.last_message as { short: string }).short
) : ( ) : (
<Text <Text
id="quantities.members" id="quantities.members"
plural={channel.recipients.length} plural={channel.recipients!.length}
fields={{ count: channel.recipients.length }} fields={{ count: channel.recipients!.length }}
/> />
)} )}
</div> </div>
...@@ -180,7 +185,7 @@ export function ChannelButton(props: ChannelProps) { ...@@ -180,7 +185,7 @@ export function ChannelButton(props: ChannelProps) {
</div> </div>
</div> </div>
); );
} });
type ButtonProps = CommonProps & { type ButtonProps = CommonProps & {
onClick?: () => void; onClick?: () => void;
......
...@@ -4,9 +4,9 @@ import { ...@@ -4,9 +4,9 @@ import {
Wrench, Wrench,
Notepad, Notepad,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, Redirect, useLocation, useParams } from "react-router-dom"; import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import { Channels } from "revolt.js/dist/api/objects"; import { RelationshipStatus } from "revolt-api/types/Users";
import { Users as UsersNS } from "revolt.js/dist/api/objects";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
...@@ -21,11 +21,6 @@ import { Unreads } from "../../../redux/reducers/unreads"; ...@@ -21,11 +21,6 @@ import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import {
useDMs,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import placeholderSVG from "../items/placeholder.svg"; import placeholderSVG from "../items/placeholder.svg";
...@@ -39,16 +34,21 @@ type Props = { ...@@ -39,16 +34,21 @@ type Props = {
unreads: Unreads; unreads: Unreads;
}; };
function HomeSidebar(props: Props) { const HomeSidebar = observer((props: Props) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const ctx = useForceUpdate(); const channels = [...client.channels.values()]
const channels = useDMs(ctx); .filter(
(x) =>
x.channel_type === "DirectMessage" ||
x.channel_type === "Group",
)
.map((x) => mapChannelWithUnread(x, props.unreads));
const obj = channels.find((x) => x?._id === channel); const obj = client.channels.get(channel);
if (channel && !obj) return <Redirect to="/" />; if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj }); if (obj) useUnreads({ ...props, channel: obj });
...@@ -62,21 +62,7 @@ function HomeSidebar(props: Props) { ...@@ -62,21 +62,7 @@ function HomeSidebar(props: Props) {
}); });
}, [channel]); }, [channel]);
const channelsArr = channels channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
.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 ( return (
<GenericSidebarBase padding> <GenericSidebarBase padding>
...@@ -98,10 +84,10 @@ function HomeSidebar(props: Props) { ...@@ -98,10 +84,10 @@ function HomeSidebar(props: Props) {
<ButtonItem <ButtonItem
active={pathname === "/friends"} active={pathname === "/friends"}
alert={ alert={
typeof users.find( typeof [...client.users.values()].find(
(user) => (user) =>
user?.relationship === user?.relationship ===
UsersNS.Relationship.Incoming, RelationshipStatus.Incoming,
) !== "undefined" ) !== "undefined"
? "unread" ? "unread"
: undefined : undefined
...@@ -143,20 +129,18 @@ function HomeSidebar(props: Props) { ...@@ -143,20 +129,18 @@ function HomeSidebar(props: Props) {
}) })
} }
/> />
{channelsArr.length === 0 && ( {channels.length === 0 && (
<img src={placeholderSVG} loading="eager" /> <img src={placeholderSVG} loading="eager" />
)} )}
{channelsArr.map((x) => { {channels.map((x) => {
let user; let user;
if (x.channel_type === "DirectMessage") { if (x.channel.channel_type === "DirectMessage") {
if (!x.active) return null; if (!x.channel.active) return null;
user = x.channel.recipient;
const recipient = client.channels.getRecipient(x._id);
user = users.find((x) => x?._id === recipient);
if (!user) { if (!user) {
console.warn( console.warn(
`Skipped DM ${x._id} because user was missing.`, `Skipped DM ${x.channel._id} because user was missing.`,
); );
return null; return null;
} }
...@@ -164,14 +148,15 @@ function HomeSidebar(props: Props) { ...@@ -164,14 +148,15 @@ function HomeSidebar(props: Props) {
return ( return (
<ConditionalLink <ConditionalLink
active={x._id === channel} key={x.channel._id}
to={`/channel/${x._id}`}> active={x.channel._id === channel}
to={`/channel/${x.channel._id}`}>
<ChannelButton <ChannelButton
user={user} user={user}
channel={x} channel={x.channel}
alert={x.unread} alert={x.unread}
alertCount={x.alertCount} alertCount={x.alertCount}
active={x._id === channel} active={x.channel._id === channel}
/> />
</ConditionalLink> </ConditionalLink>
); );
...@@ -180,7 +165,7 @@ function HomeSidebar(props: Props) { ...@@ -180,7 +165,7 @@ function HomeSidebar(props: Props) {
</GenericSidebarList> </GenericSidebarList>
</GenericSidebarBase> </GenericSidebarBase>
); );
} });
export default connectState( export default connectState(
HomeSidebar, HomeSidebar,
......
import { Plus } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { useLocation, useParams } from "react-router-dom"; import { observer } from "mobx-react-lite";
import { Channel, Servers } from "revolt.js/dist/api/objects"; import { useHistory, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { attachContextMenu, openContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
...@@ -14,14 +15,8 @@ import { LastOpened } from "../../../redux/reducers/last_opened"; ...@@ -14,14 +15,8 @@ import { LastOpened } from "../../../redux/reducers/last_opened";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useChannels,
useForceUpdate,
useSelf,
useServers,
} from "../../../context/revoltjs/hooks";
import logoSVG from "../../../assets/logo.svg";
import ServerIcon from "../../common/ServerIcon"; import ServerIcon from "../../common/ServerIcon";
import Tooltip from "../../common/Tooltip"; import Tooltip from "../../common/Tooltip";
import UserHover from "../../common/user/UserHover"; import UserHover from "../../common/user/UserHover";
...@@ -56,7 +51,7 @@ function Icon({ ...@@ -56,7 +51,7 @@ function Icon({
<circle cx="27" cy="5" r="5" fill={"white"} /> <circle cx="27" cy="5" r="5" fill={"white"} />
)} )}
{unread === "mention" && ( {unread === "mention" && (
<circle cx="27" cy="5" r="5" fill={"red"} /> <circle cx="27" cy="5" r="5" fill={"var(--error)"} />
)} )}
</svg> </svg>
); );
...@@ -65,6 +60,7 @@ function Icon({ ...@@ -65,6 +60,7 @@ function Icon({
const ServersBase = styled.div` const ServersBase = styled.div`
width: 56px; width: 56px;
height: 100%; height: 100%;
padding-left: 2px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -78,7 +74,8 @@ const ServerList = styled.div` ...@@ -78,7 +74,8 @@ const ServerList = styled.div`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
overflow-y: scroll; overflow-y: scroll;
padding-bottom: 48px; padding-bottom: 20px;
/*width: 58px;*/
flex-direction: column; flex-direction: column;
scrollbar-width: none; scrollbar-width: none;
...@@ -90,6 +87,11 @@ const ServerList = styled.div` ...@@ -90,6 +87,11 @@ const ServerList = styled.div`
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0px; width: 0px;
} }
/*${isTouchscreenDevice &&
css`
width: 58px;
`}*/
`; `;
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
...@@ -97,10 +99,13 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -97,10 +99,13 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
:focus {
outline: 3px solid blue;
}
> div { > div {
height: 42px; height: 42px;
padding-left: 4px; padding-inline-start: 6px;
padding-right: 6px;
display: grid; display: grid;
place-items: center; place-items: center;
...@@ -132,11 +137,7 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -132,11 +137,7 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
`} `}
svg { svg {
width: 57px; margin-top: 5px;
height: 117px;
margin-top: 4px;
display: relative;
pointer-events: none; pointer-events: none;
// outline: 1px solid red; // outline: 1px solid red;
} }
...@@ -153,13 +154,22 @@ function Swoosh() { ...@@ -153,13 +154,22 @@ function Swoosh() {
return ( return (
<span> <span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" width="54"
width="57" height="106"
height="117" viewBox="0 0 54 106"
fill="var(--sidebar-active)"> xmlns="http://www.w3.org/2000/svg">
<path d="M27.746 86.465c14 0 28 11.407 28 28s.256-56 .256-56-42.256 28-28.256 28z" /> <path
<path d="M56 58.465c0 15.464-12.536 28-28 28s-28-12.536-28-28 12.536-28 28-28 28 12.536 28 28z" /> d="M54 53C54 67.9117 41.9117 80 27 80C12.0883 80 0 67.9117 0 53C0 38.0883 12.0883 26 27 26C41.9117 26 54 38.0883 54 53Z"
<path d="M28.002 30.465c14 0 28-11.407 28-28s0 56 0 56-42-28-28-28z" /> fill="var(--sidebar-active)"
/>
<path
d="M27 80C4.5 80 54 53 54 53L54.0001 106C54.0001 106 49.5 80 27 80Z"
fill="var(--sidebar-active)"
/>
<path
d="M27 26C4.5 26 54 53 54 53L53.9999 0C53.9999 0 49.5 26 27 26Z"
fill="var(--sidebar-active)"
/>
</svg> </svg>
</span> </span>
); );
...@@ -170,28 +180,32 @@ interface Props { ...@@ -170,28 +180,32 @@ interface Props {
lastOpened: LastOpened; lastOpened: LastOpened;
} }
export function ServerListSidebar({ unreads, lastOpened }: Props) { export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
const ctx = useForceUpdate(); const client = useClient();
const self = useSelf(ctx);
const activeServers = useServers(undefined, ctx) as Servers.Server[]; const { server: server_id } = useParams<{ server?: string }>();
const channels = (useChannels(undefined, ctx) as Channel[]).map((x) => const server = server_id ? client.servers.get(server_id) : undefined;
const activeServers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) =>
mapChannelWithUnread(x, unreads), mapChannelWithUnread(x, unreads),
); );
const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id); const unreadChannels = channels
.filter((x) => x.unread)
.map((x) => x.channel?._id);
const servers = activeServers.map((server) => { const servers = activeServers.map((server) => {
let alertCount = 0; let alertCount = 0;
for (const id of server.channels) { for (const id of server.channel_ids) {
const channel = channels.find((x) => x._id === id); const channel = channels.find((x) => x.channel?._id === id);
if (channel?.alertCount) { if (channel?.alertCount) {
alertCount += channel.alertCount; alertCount += channel.alertCount;
} }
} }
return { return {
...server, server,
unread: (typeof server.channels.find((x) => unread: (typeof server.channel_ids.find((x) =>
unreadChannels.includes(x), unreadChannels.includes(x),
) !== "undefined" ) !== "undefined"
? alertCount > 0 ? alertCount > 0
...@@ -202,18 +216,17 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -202,18 +216,17 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
}; };
}); });
const history = useHistory();
const path = useLocation().pathname; 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 homeUnread: "mention" | "unread" | undefined;
let alertCount = 0; let alertCount = 0;
for (const x of channels) { for (const x of channels) {
if ( if (
((x.channel_type === "DirectMessage" && x.active) || (x.channel?.channel_type === "DirectMessage"
x.channel_type === "Group") && ? x.channel?.active
: x.channel?.channel_type === "Group") &&
x.unread x.unread
) { ) {
homeUnread = "unread"; homeUnread = "unread";
...@@ -221,6 +234,14 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -221,6 +234,14 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
} }
} }
if (
[...client.users.values()].find(
(x) => x.relationship === RelationshipStatus.Incoming,
)
) {
alertCount++;
}
if (alertCount > 0) homeUnread = "mention"; if (alertCount > 0) homeUnread = "mention";
const homeActive = const homeActive =
typeof server === "undefined" && !path.startsWith("/invite"); typeof server === "undefined" && !path.startsWith("/invite");
...@@ -236,14 +257,15 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -236,14 +257,15 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
<div <div
onContextMenu={attachContextMenu("Status")} onContextMenu={attachContextMenu("Status")}
onClick={() => onClick={() =>
homeActive && openContextMenu("Status") homeActive && history.push("/settings")
}> }>
<UserHover user={self}> <UserHover user={client.user}>
<Icon size={42} unread={homeUnread}> <Icon size={42} unread={homeUnread}>
<UserIcon <UserIcon
target={self} target={client.user}
size={32} size={32}
status status
hover
/> />
</Icon> </Icon>
</UserHover> </UserHover>
...@@ -252,24 +274,30 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -252,24 +274,30 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
</ConditionalLink> </ConditionalLink>
<LineDivider /> <LineDivider />
{servers.map((entry) => { {servers.map((entry) => {
const active = entry!._id === server?._id; const active = entry.server._id === server?._id;
const id = lastOpened[entry!._id]; const id = lastOpened[entry.server._id];
return ( return (
<ConditionalLink <ConditionalLink
key={entry.server._id}
active={active} active={active}
to={`/server/${entry!._id}${ to={`/server/${entry.server._id}${
id ? `/channel/${id}` : "" id ? `/channel/${id}` : ""
}`}> }`}>
<ServerEntry <ServerEntry
active={active} active={active}
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
server: entry!._id, server: entry.server._id,
})}> })}>
<Swoosh /> <Swoosh />
<Tooltip content={entry.name} placement="right"> <Tooltip
content={entry.server.name}
placement="right">
<Icon size={42} unread={entry.unread}> <Icon size={42} unread={entry.unread}>
<ServerIcon size={32} target={entry} /> <ServerIcon
size={32}
target={entry.server}
/>
</Icon> </Icon>
</Tooltip> </Tooltip>
</ServerEntry> </ServerEntry>
...@@ -285,11 +313,20 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -285,11 +313,20 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
}> }>
<Plus size={36} /> <Plus size={36} />
</IconButton> </IconButton>
{/*<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_server",
})
}>
<Compass size={36} />
</IconButton>*/}
<PaintCounter small /> <PaintCounter small />
</ServerList> </ServerList>
</ServersBase> </ServersBase>
); );
} });
export default connectState(ServerListSidebar, (state) => { export default connectState(ServerListSidebar, (state) => {
return { return {
......
import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { Channels } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components";
import styled from "styled-components";
import { attachContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useChannels,
useForceUpdate,
useServer,
} from "../../../context/revoltjs/hooks";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader"; import ServerHeader from "../../common/ServerHeader";
...@@ -37,10 +34,13 @@ const ServerBase = styled.div` ...@@ -37,10 +34,13 @@ const ServerBase = styled.div`
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;
background: var(--secondary-background); background: var(--secondary-background);
border-start-start-radius: 8px; border-start-start-radius: 8px;
border-end-start-radius: 8px;
overflow: hidden; overflow: hidden;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`; `;
const ServerList = styled.div` const ServerList = styled.div`
...@@ -53,23 +53,17 @@ const ServerList = styled.div` ...@@ -53,23 +53,17 @@ const ServerList = styled.div`
} }
`; `;
function ServerSidebar(props: Props) { const ServerSidebar = observer((props: Props) => {
const client = useClient();
const { server: server_id, channel: channel_id } = const { server: server_id, channel: channel_id } =
useParams<{ server?: string; channel?: string }>(); useParams<{ server: string; channel?: string }>();
const ctx = useForceUpdate();
const server = useServer(server_id, ctx); const server = client.servers.get(server_id);
if (!server) return <Redirect to="/" />; if (!server) return <Redirect to="/" />;
const channels = ( const channel = channel_id ? client.channels.get(channel_id) : undefined;
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_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel }, ctx); if (channel) useUnreads({ ...props, channel });
useEffect(() => { useEffect(() => {
if (!channel_id) return; if (!channel_id) return;
...@@ -79,13 +73,13 @@ function ServerSidebar(props: Props) { ...@@ -79,13 +73,13 @@ function ServerSidebar(props: Props) {
parent: server_id!, parent: server_id!,
child: channel_id!, child: channel_id!,
}); });
}, [channel_id]); }, [channel_id, server_id]);
const uncategorised = new Set(server.channels); const uncategorised = new Set(server.channel_ids);
const elements = []; const elements = [];
function addChannel(id: string) { function addChannel(id: string) {
const entry = channels.find((x) => x._id === id); const entry = client.channels.get(id);
if (!entry) return; if (!entry) return;
const active = channel?._id === entry._id; const active = channel?._id === entry._id;
...@@ -98,7 +92,8 @@ function ServerSidebar(props: Props) { ...@@ -98,7 +92,8 @@ function ServerSidebar(props: Props) {
<ChannelButton <ChannelButton
channel={entry} channel={entry}
active={active} active={active}
alert={entry.unread} // ! FIXME: pull it out directly
alert={mapChannelWithUnread(entry, props.unreads).unread}
compact compact
/> />
</ConditionalLink> </ConditionalLink>
...@@ -130,7 +125,7 @@ function ServerSidebar(props: Props) { ...@@ -130,7 +125,7 @@ function ServerSidebar(props: Props) {
return ( return (
<ServerBase> <ServerBase>
<ServerHeader server={server} ctx={ctx} /> <ServerHeader server={server} />
<ConnectionStatus /> <ConnectionStatus />
<ServerList <ServerList
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
...@@ -141,7 +136,7 @@ function ServerSidebar(props: Props) { ...@@ -141,7 +136,7 @@ function ServerSidebar(props: Props) {
<PaintCounter small /> <PaintCounter small />
</ServerBase> </ServerBase>
); );
} });
export default connectState(ServerSidebar, (state) => { export default connectState(ServerSidebar, (state) => {
return { return {
......
import { Channel } from "revolt.js"; import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect } from "preact/hooks"; import { useLayoutEffect } from "preact/hooks";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
type UnreadProps = { type UnreadProps = {
channel: Channel; channel: Channel;
unreads: Unreads; unreads: Unreads;
}; };
export function useUnreads( export function useUnreads({ channel, unreads }: UnreadProps) {
{ channel, unreads }: UnreadProps,
context?: HookContext,
) {
const ctx = useForceUpdate(context);
useLayoutEffect(() => { useLayoutEffect(() => {
function checkUnread(target?: Channel) { function checkUnread(target: Channel) {
if (!target) return; if (!target) return;
if (target._id !== channel._id) return; if (target._id !== channel._id) return;
if ( if (
...@@ -41,19 +35,16 @@ export function useUnreads( ...@@ -41,19 +35,16 @@ export function useUnreads(
message, message,
}); });
ctx.client.req( channel.ack(message);
"PUT",
`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
);
} }
} }
} }
checkUnread(channel); checkUnread(channel);
return reaction(
ctx.client.channels.addListener("mutation", checkUnread); () => channel.last_message,
return () => () => checkUnread(channel),
ctx.client.channels.removeListener("mutation", checkUnread); );
}, [channel, unreads]); }, [channel, unreads]);
} }
...@@ -63,12 +54,12 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -63,12 +54,12 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
channel.channel_type === "DirectMessage" || channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group" channel.channel_type === "Group"
) { ) {
last_message_id = channel.last_message?._id; last_message_id = (channel.last_message as { _id: string })?._id;
} else if (channel.channel_type === "TextChannel") { } else if (channel.channel_type === "TextChannel") {
last_message_id = channel.last_message; last_message_id = channel.last_message as string;
} else { } else {
return { return {
...channel, channel,
unread: undefined, unread: undefined,
alertCount: undefined, alertCount: undefined,
timestamp: channel._id, timestamp: channel._id,
...@@ -85,7 +76,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -85,7 +76,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
unread = "mention"; unread = "mention";
} else if ( } else if (
u.last_id && u.last_id &&
last_message_id.localeCompare(u.last_id) > 0 (last_message_id as string).localeCompare(u.last_id) > 0
) { ) {
unread = "unread"; unread = "unread";
} }
...@@ -95,7 +86,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -95,7 +86,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
} }
return { return {
...channel, channel,
timestamp: last_message_id ?? channel._id, timestamp: last_message_id ?? channel._id,
unread, unread,
alertCount, alertCount,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useRenderState } from "../../../lib/renderer/Singleton"; import { useRenderState } from "../../../lib/renderer/Singleton";
interface Props { interface Props {
......
import { useParams } from "react-router"; /* eslint-disable react-hooks/rules-of-hooks */
import { Link } from "react-router-dom"; import { observer } from "mobx-react-lite";
import { User } from "revolt.js"; import { Link, useParams } from "react-router-dom";
import { Channels, Message, Servers, Users } from "revolt.js/dist/api/objects"; import { Presence } from "revolt-api/types/Users";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
...@@ -11,16 +12,10 @@ import { getState } from "../../../redux"; ...@@ -11,16 +12,10 @@ import { getState } from "../../../redux";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import {
AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient"; } from "../../../context/revoltjs/RevoltClient";
import {
HookContext,
useChannel,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import Button from "../../ui/Button"; import Button from "../../ui/Button";
...@@ -33,36 +28,30 @@ import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; ...@@ -33,36 +28,30 @@ import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import { UserButton } from "../items/ButtonItem"; import { UserButton } from "../items/ButtonItem";
import { ChannelDebugInfo } from "./ChannelDebugInfo"; import { ChannelDebugInfo } from "./ChannelDebugInfo";
interface Props { export default function MemberSidebar({ channel: obj }: { channel?: Channel }) {
ctx: HookContext; const { channel: channel_id } = useParams<{ channel: string }>();
} const client = useClient();
const channel = obj ?? client.channels.get(channel_id);
export default function MemberSidebar(props: { channel?: Channels.Channel }) {
const ctx = useForceUpdate();
const { channel: cid } = useParams<{ channel: string }>();
const channel = props.channel ?? useChannel(cid, ctx);
switch (channel?.channel_type) { switch (channel?.channel_type) {
case "Group": case "Group":
return <GroupMemberSidebar channel={channel} ctx={ctx} />; return <GroupMemberSidebar channel={channel} />;
case "TextChannel": case "TextChannel":
return <ServerMemberSidebar channel={channel} ctx={ctx} />; return <ServerMemberSidebar channel={channel} />;
default: default:
return null; return null;
} }
} }
export function GroupMemberSidebar({ export const GroupMemberSidebar = observer(
channel, ({ channel }: { channel: Channel }) => {
ctx, const { openScreen } = useIntermediate();
}: Props & { channel: Channels.GroupChannel }) {
const { openScreen } = useIntermediate(); const members = channel.recipients?.filter(
const users = useUsers(undefined, ctx); (x) => typeof x !== "undefined",
const 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; const voiceActive = voice.roomId === channel._id;
let voiceParticipants: User[] = []; let voiceParticipants: User[] = [];
...@@ -77,34 +66,34 @@ export function GroupMemberSidebar({ ...@@ -77,34 +66,34 @@ export function GroupMemberSidebar({
voiceParticipants.sort((a, b) => a.username.localeCompare(b.username)); voiceParticipants.sort((a, b) => a.username.localeCompare(b.username));
}*/ }*/
members.sort((a, b) => { members?.sort((a, b) => {
// ! FIXME: should probably rewrite all this code // ! FIXME: should probably rewrite all this code
const l = const l =
+( +(
(a.online && a.status?.presence !== Users.Presence.Invisible) ?? (a!.online && a!.status?.presence !== Presence.Invisible) ??
false false
) | 0; ) | 0;
const r = const r =
+( +(
(b.online && b.status?.presence !== Users.Presence.Invisible) ?? (b!.online && b!.status?.presence !== Presence.Invisible) ??
false false
) | 0; ) | 0;
const n = r - l; const n = r - l;
if (n !== 0) { if (n !== 0) {
return n; return n;
} }
return a.username.localeCompare(b.username);
});
return ( return a!.username.localeCompare(b!.username);
<GenericSidebarBase> });
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} /> return (
<Search channel={channel._id} /> <GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
<Search channel={channel} />
{/*voiceActive && voiceParticipants.length !== 0 && ( {/*voiceActive && voiceParticipants.length !== 0 && (
<Fragment> <Fragment>
<Category <Category
type="members" type="members"
...@@ -129,146 +118,31 @@ export function GroupMemberSidebar({ ...@@ -129,146 +118,31 @@ export function GroupMemberSidebar({
)} )}
</Fragment> </Fragment>
)*/} )*/}
<CollapsibleSection
sticky
id="members"
defaultValue
summary={
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" />{" "}
{channel.recipients.length}
</span>
}
/>
}>
{members.length === 0 && (
<img src={placeholderSVG} loading="eager" />
)}
{members.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</CollapsibleSection>
</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);
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
),
),
);
}
}
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
const l =
+(
(a.online && a.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
const r =
+(
(b.online && b.status?.presence !== Users.Presence.Invisible) ??
false
) | 0;
const n = r - l;
if (n !== 0) {
return n;
}
return a.username.localeCompare(b.username);
});
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
<Search channel={channel._id} />
<div>{!members && <Preloader type="ring" />}</div>
{members && (
<CollapsibleSection <CollapsibleSection
//sticky //will re-add later, need to fix css sticky
id="members" id="members"
defaultValue defaultValue
summary={ summary={
<span> <Category
<Text id="app.main.categories.members" />{" "} variant="uniform"
{users.length} text={
</span> <span>
<Text id="app.main.categories.members" />{" "}
{channel.recipients?.length ?? 0}
</span>
}
/>
}> }>
{users.length === 0 && ( {members?.length === 0 && (
<img src={placeholderSVG} loading="eager" /> <img src={placeholderSVG} loading="eager" />
)} )}
{users.map( {members?.map(
(user) => (user) =>
user && ( user && (
<UserButton <UserButton
key={user._id} key={user._id}
user={user} user={user}
context={channel} context={channel!}
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "profile", id: "profile",
...@@ -279,16 +153,96 @@ export function ServerMemberSidebar({ ...@@ -279,16 +153,96 @@ export function ServerMemberSidebar({
), ),
)} )}
</CollapsibleSection> </CollapsibleSection>
)} </GenericSidebarList>
</GenericSidebarList> </GenericSidebarBase>
</GenericSidebarBase> );
); },
} );
export const ServerMemberSidebar = observer(
({ channel }: { channel: Channel }) => {
const client = useClient();
const { openScreen } = useIntermediate();
const status = useContext(StatusContext);
useEffect(() => {
if (status === ClientStatus.ONLINE) {
channel.server!.fetchMembers();
}
}, [status, channel.server]);
const users = [...client.members.keys()]
.map((x) => JSON.parse(x))
.filter((x) => x.server === channel.server_id)
.map((y) => client.users.get(y.user)!)
.filter((z) => typeof z !== "undefined");
// copy paste from above
users.sort((a, b) => {
// ! FIXME: should probably rewrite all this code
const l =
+(
(a.online && a.status?.presence !== Presence.Invisible) ??
false
) | 0;
const r =
+(
(b.online && b.status?.presence !== Presence.Invisible) ??
false
) | 0;
const n = r - l;
if (n !== 0) {
return n;
}
function Search({ channel }: { channel: string }) { return a.username.localeCompare(b.username);
});
return (
<GenericSidebarBase>
<GenericSidebarList>
<ChannelDebugInfo id={channel._id} />
<Search channel={channel} />
<div>{users.length === 0 && <Preloader type="ring" />}</div>
{users.length > 0 && (
<CollapsibleSection
//sticky //will re-add later, need to fix css
id="members"
defaultValue
summary={
<span>
<Text id="app.main.categories.members" />{" "}
{users?.length ?? 0}
</span>
}>
{users.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</CollapsibleSection>
)}
</GenericSidebarList>
</GenericSidebarBase>
);
},
);
function Search({ channel }: { channel: Channel }) {
if (!getState().experiments.enabled?.includes("search")) return null; if (!getState().experiments.enabled?.includes("search")) return null;
const client = useContext(AppContext);
type Sort = "Relevance" | "Latest" | "Oldest"; type Sort = "Relevance" | "Latest" | "Oldest";
const [sort, setSort] = useState<Sort>("Relevance"); const [sort, setSort] = useState<Sort>("Relevance");
...@@ -296,11 +250,7 @@ function Search({ channel }: { channel: string }) { ...@@ -296,11 +250,7 @@ function Search({ channel }: { channel: string }) {
const [results, setResults] = useState<Message[]>([]); const [results, setResults] = useState<Message[]>([]);
async function search() { async function search() {
const data = await client.channels.searchWithUsers( const data = await channel.searchWithUsers({ query, sort });
channel,
{ query, sort },
true,
);
setResults(data.messages); setResults(data.messages);
} }
...@@ -317,6 +267,7 @@ function Search({ channel }: { channel: string }) { ...@@ -317,6 +267,7 @@ function Search({ channel }: { channel: string }) {
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
{["Relevance", "Latest", "Oldest"].map((key) => ( {["Relevance", "Latest", "Oldest"].map((key) => (
<Button <Button
key={key}
style={{ flex: 1, minWidth: 0 }} style={{ flex: 1, minWidth: 0 }}
compact compact
error={sort === key} error={sort === key}
...@@ -342,25 +293,21 @@ function Search({ channel }: { channel: string }) { ...@@ -342,25 +293,21 @@ function Search({ channel }: { channel: string }) {
}}> }}>
{results.map((message) => { {results.map((message) => {
let href = ""; let href = "";
const channel = client.channels.get(message.channel);
if (channel?.channel_type === "TextChannel") { if (channel?.channel_type === "TextChannel") {
href += `/server/${channel.server}`; href += `/server/${channel.server_id}`;
} }
href += `/channel/${message.channel}/${message._id}`; href += `/channel/${message.channel_id}/${message._id}`;
return ( return (
<Link to={href}> <Link to={href} key={message._id}>
<div <div
style={{ style={{
margin: "2px", margin: "2px",
padding: "6px", padding: "6px",
background: "var(--primary-background)", background: "var(--primary-background)",
}}> }}>
<b> <b>@{message.author?.username}</b>
@
{client.users.get(message.author)?.username}
</b>
<br /> <br />
{message.content} {message.content}
</div> </div>
......
...@@ -6,6 +6,7 @@ interface Props { ...@@ -6,6 +6,7 @@ interface Props {
readonly contrast?: boolean; readonly contrast?: boolean;
readonly plain?: boolean; readonly plain?: boolean;
readonly error?: boolean; readonly error?: boolean;
readonly gold?: boolean;
readonly iconbutton?: boolean; readonly iconbutton?: boolean;
} }
...@@ -125,4 +126,22 @@ export default styled.button<Props>` ...@@ -125,4 +126,22 @@ export default styled.button<Props>`
background: var(--error); background: var(--error);
} }
`} `}
${(props) =>
props.gold &&
css`
color: black;
font-weight: 600;
background: goldenrod;
&:hover {
filter: brightness(1.2);
background: goldenrod;
}
&:disabled {
cursor: not-allowed;
background: goldenrod;
}
`}
`; `;
/* eslint-disable react-hooks/rules-of-hooks */
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import { createPortal, useEffect, useState } from "preact/compat"; import { createPortal, useCallback, useEffect, useState } from "preact/compat";
import { internalSubscribe } from "../../lib/eventEmitter"; import { internalSubscribe } from "../../lib/eventEmitter";
...@@ -134,7 +135,7 @@ interface Props { ...@@ -134,7 +135,7 @@ interface Props {
dontModal?: boolean; dontModal?: boolean;
padding?: boolean; padding?: boolean;
onClose: () => void; onClose?: () => void;
actions?: Action[]; actions?: Action[];
disabled?: boolean; disabled?: boolean;
border?: boolean; border?: boolean;
...@@ -163,12 +164,12 @@ export default function Modal(props: Props) { ...@@ -163,12 +164,12 @@ export default function Modal(props: Props) {
const [animateClose, setAnimateClose] = useState(false); const [animateClose, setAnimateClose] = useState(false);
isModalClosing = animateClose; isModalClosing = animateClose;
function onClose() { const onClose = useCallback(() => {
setAnimateClose(true); setAnimateClose(true);
setTimeout(() => props.onClose(), 2e2); setTimeout(() => props.onClose?.(), 2e2);
} }, [setAnimateClose, props]);
useEffect(() => internalSubscribe("Modal", "close", onClose), []); useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]);
useEffect(() => { useEffect(() => {
if (props.disallowClosing) return; if (props.disallowClosing) return;
...@@ -181,7 +182,7 @@ export default function Modal(props: Props) { ...@@ -181,7 +182,7 @@ export default function Modal(props: Props) {
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, [props.disallowClosing, props.onClose]); }, [props.disallowClosing, onClose]);
const confirmationAction = props.actions?.find( const confirmationAction = props.actions?.find(
(action) => action.confirmation, (action) => action.confirmation,
...@@ -190,7 +191,7 @@ export default function Modal(props: Props) { ...@@ -190,7 +191,7 @@ export default function Modal(props: Props) {
useEffect(() => { useEffect(() => {
if (!confirmationAction) return; if (!confirmationAction) return;
// ! FIXME: this may be done better if we // ! TODO: this may be done better if we
// ! can focus the button although that // ! can focus the button although that
// ! doesn't seem to work... // ! doesn't seem to work...
function keyDown(e: KeyboardEvent) { function keyDown(e: KeyboardEvent) {
...@@ -211,8 +212,12 @@ export default function Modal(props: Props) { ...@@ -211,8 +212,12 @@ export default function Modal(props: Props) {
{content} {content}
{props.actions && ( {props.actions && (
<ModalActions> <ModalActions>
{props.actions.map((x) => ( {props.actions.map((x, index) => (
<Button {...x} disabled={props.disabled} /> <Button
key={index}
{...x}
disabled={props.disabled}
/>
))} ))}
</ModalActions> </ModalActions>
)} )}
......
...@@ -8,13 +8,19 @@ type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, "children" | "as"> & { ...@@ -8,13 +8,19 @@ type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, "children" | "as"> & {
error?: string; error?: string;
block?: boolean; block?: boolean;
spaced?: boolean; spaced?: boolean;
noMargin?: boolean;
children?: Children; children?: Children;
type?: "default" | "subtle" | "error"; type?: "default" | "subtle" | "error";
}; };
const OverlineBase = styled.div<Omit<Props, "children" | "error">>` const OverlineBase = styled.div<Omit<Props, "children" | "error">>`
display: inline; display: inline;
margin: 0.4em 0;
${(props) =>
!props.noMargin &&
css`
margin: 0.4em 0;
`}
${(props) => ${(props) =>
props.spaced && props.spaced &&
......
...@@ -48,7 +48,7 @@ export default styled.textarea<TextAreaProps>` ...@@ -48,7 +48,7 @@ export default styled.textarea<TextAreaProps>`
${(props) => ${(props) =>
props.code props.code
? css` ? css`
font-family: var(--monoscape-font), monospace; font-family: var(--monospace-font), monospace;
` `
: css` : css`
font-family: inherit; font-family: inherit;
......
...@@ -55,14 +55,16 @@ export const TipBase = styled.div<Props>` ...@@ -55,14 +55,16 @@ export const TipBase = styled.div<Props>`
`} `}
`; `;
export default function Tip(props: Props & { children: Children }) { export default function Tip(
const { children, ...tipProps } = props; props: Props & { children: Children; hideSeparator?: boolean },
) {
const { children, hideSeparator, ...tipProps } = props;
return ( return (
<> <>
<Separator /> {!hideSeparator && <Separator />}
<TipBase {...tipProps}> <TipBase {...tipProps}>
<InfoCircle size={20} /> <InfoCircle size={20} />
<span>{props.children}</span> <span>{children}</span>
</TipBase> </TipBase>
</> </>
); );
......
import { ChevronRight, LinkExternal } from "@styled-icons/boxicons-regular";
import styled, { css } from "styled-components";
import { Children } from "../../../types/Preact";
interface BaseProps {
readonly hover?: boolean;
readonly account?: boolean;
readonly disabled?: boolean;
readonly largeDescription?: boolean;
}
const CategoryBase = styled.div<BaseProps>`
/*height: 54px;*/
padding: 9.8px 12px;
border-radius: 6px;
margin-bottom: 10px;
color: var(--foreground);
background: var(--secondary-header);
gap: 12px;
display: flex;
align-items: center;
flex-direction: row;
> svg {
flex-shrink: 0;
}
.content {
display: flex;
flex-grow: 1;
flex-direction: column;
font-weight: 600;
font-size: 14px;
.title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.description {
${(props) =>
props.largeDescription
? css`
font-size: 14px;
`
: css`
font-size: 11px;
`}
font-weight: 400;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
a:hover {
text-decoration: underline;
}
}
}
${(props) =>
props.hover &&
css`
cursor: pointer;
opacity: 1;
transition: 0.1s ease background-color;
&:hover {
background: var(--secondary-background);
}
`}
${(props) =>
props.disabled &&
css`
opacity: 0.4;
/*.content,
.action {
color: var(--tertiary-foreground);
}*/
.action {
font-size: 14px;
}
`}
${(props) =>
props.account &&
css`
height: 54px;
.content {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.title {
text-transform: uppercase;
font-size: 12px;
color: var(--secondary-foreground);
}
.description {
font-size: 15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
`}
`;
interface Props extends BaseProps {
icon?: Children;
children?: Children;
description?: Children;
onClick?: () => void;
action?: "chevron" | "external" | Children;
}
export default function CategoryButton({
icon,
children,
description,
account,
disabled,
onClick,
hover,
action,
}: Props) {
return (
<CategoryBase
hover={hover || typeof onClick !== "undefined"}
onClick={onClick}
disabled={disabled}
account={account}>
{icon}
<div class="content">
<div className="title">{children}</div>
<div className="description">{description}</div>
</div>
<div class="action">
{typeof action === "string" ? (
action === "chevron" ? (
<ChevronRight size={24} />
) : (
<LinkExternal size={20} />
)
) : (
action
)}
</div>
</CategoryBase>
);
}
...@@ -5,7 +5,7 @@ import update from "dayjs/plugin/updateLocale"; ...@@ -5,7 +5,7 @@ import update from "dayjs/plugin/updateLocale";
import defaultsDeep from "lodash.defaultsdeep"; import defaultsDeep from "lodash.defaultsdeep";
import { IntlProvider } from "preact-i18n"; import { IntlProvider } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useCallback, useEffect, useState } from "preact/hooks";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
...@@ -32,6 +32,7 @@ export enum Language { ...@@ -32,6 +32,7 @@ export enum Language {
CROATIAN = "hr", CROATIAN = "hr",
HUNGARIAN = "hu", HUNGARIAN = "hu",
INDONESIAN = "id", INDONESIAN = "id",
ITALIAN = "it",
LITHUANIAN = "lt", LITHUANIAN = "lt",
MACEDONIAN = "mk", MACEDONIAN = "mk",
DUTCH = "nl", DUTCH = "nl",
...@@ -41,6 +42,7 @@ export enum Language { ...@@ -41,6 +42,7 @@ export enum Language {
RUSSIAN = "ru", RUSSIAN = "ru",
SERBIAN = "sr", SERBIAN = "sr",
SWEDISH = "sv", SWEDISH = "sv",
TOKIPONA = "tokipona",
TURKISH = "tr", TURKISH = "tr",
UKRANIAN = "uk", UKRANIAN = "uk",
CHINESE_SIMPLIFIED = "zh_Hans", CHINESE_SIMPLIFIED = "zh_Hans",
...@@ -57,7 +59,7 @@ export interface LanguageEntry { ...@@ -57,7 +59,7 @@ export interface LanguageEntry {
i18n: string; i18n: string;
dayjs?: string; dayjs?: string;
rtl?: boolean; rtl?: boolean;
alt?: boolean; cat?: "const" | "alt";
} }
export const Languages: { [key in Language]: LanguageEntry } = { export const Languages: { [key in Language]: LanguageEntry } = {
...@@ -78,8 +80,9 @@ export const Languages: { [key in Language]: LanguageEntry } = { ...@@ -78,8 +80,9 @@ export const Languages: { [key in Language]: LanguageEntry } = {
fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" }, fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" }, hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" }, hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" }, hu: { display: "Magyar", emoji: "🇭🇺", i18n: "hu" },
id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" }, id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
it: { display: "Italiano", emoji: "🇮🇹", i18n: "it" },
lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" }, lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" }, mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" }, nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
...@@ -103,33 +106,41 @@ export const Languages: { [key in Language]: LanguageEntry } = { ...@@ -103,33 +106,41 @@ export const Languages: { [key in Language]: LanguageEntry } = {
dayjs: "zh", dayjs: "zh",
}, },
tokipona: {
display: "Toki Pona",
emoji: "🙂",
i18n: "tokipona",
dayjs: "en-gb",
cat: "const",
},
owo: { owo: {
display: "OwO", display: "OwO",
emoji: "🐱", emoji: "🐱",
i18n: "owo", i18n: "owo",
dayjs: "en-gb", dayjs: "en-gb",
alt: true, cat: "alt",
}, },
pr: { pr: {
display: "Pirate", display: "Pirate",
emoji: "🏴‍☠️", emoji: "🏴‍☠️",
i18n: "pr", i18n: "pr",
dayjs: "en-gb", dayjs: "en-gb",
alt: true, cat: "alt",
}, },
bottom: { bottom: {
display: "Bottom", display: "Bottom",
emoji: "🥺", emoji: "🥺",
i18n: "bottom", i18n: "bottom",
dayjs: "en-gb", dayjs: "en-gb",
alt: true, cat: "alt",
}, },
piglatin: { piglatin: {
display: "Pig Latin", display: "Pig Latin",
emoji: "🐖", emoji: "🐖",
i18n: "piglatin", i18n: "piglatin",
dayjs: "en-gb", dayjs: "en-gb",
alt: true, cat: "alt",
}, },
}; };
...@@ -138,35 +149,62 @@ interface Props { ...@@ -138,35 +149,62 @@ interface Props {
locale: Language; locale: Language;
} }
export interface Dictionary {
dayjs?: {
defaults?: {
twelvehour?: "yes" | "no";
separator?: string;
date?: "traditional" | "simplified" | "ISO8601";
};
timeFormat?: string;
};
[key: string]:
| Record<string, Omit<Dictionary, "dayjs">>
| string
| undefined;
}
function Locale({ children, locale }: Props) { function Locale({ children, locale }: Props) {
// TODO: create and use LanguageDefinition type here const [defns, setDefinition] = useState<Dictionary>(
const [defns, setDefinition] = definition as Dictionary,
useState<Record<string, unknown>>(definition); );
const lang = Languages[locale];
// Load relevant language information, fallback to English if invalid.
const lang = Languages[locale] ?? Languages.en;
// TODO: clean this up and use the built in Intl API function transformLanguage(source: Dictionary) {
function transformLanguage(source: { [key: string]: any }) { // Fallback untranslated strings to English (UK)
const obj = defaultsDeep(source, definition); const obj = defaultsDeep(source, definition);
const dayjs = obj.dayjs;
const defaults = dayjs.defaults;
// Take relevant objects out, dayjs and defaults
// should exist given we just took defaults above.
const { dayjs } = obj;
const { defaults } = dayjs;
// Determine whether we are using 12-hour clock.
const twelvehour = defaults?.twelvehour const twelvehour = defaults?.twelvehour
? defaults.twelvehour === "yes" ? defaults.twelvehour === "yes"
: false; : false;
// Determine what date separator we are using.
const separator: string = defaults?.date_separator ?? "/"; const separator: string = defaults?.date_separator ?? "/";
// Determine what date format we are using.
const date: "traditional" | "simplified" | "ISO8601" = const date: "traditional" | "simplified" | "ISO8601" =
defaults?.date_format ?? "traditional"; defaults?.date_format ?? "traditional";
// Available date formats.
const DATE_FORMATS = { const DATE_FORMATS = {
traditional: `DD${separator}MM${separator}YYYY`, traditional: `DD${separator}MM${separator}YYYY`,
simplified: `MM${separator}DD${separator}YYYY`, simplified: `MM${separator}DD${separator}YYYY`,
ISO8601: "YYYY-MM-DD", ISO8601: "YYYY-MM-DD",
}; };
dayjs["sameElse"] = DATE_FORMATS[date]; // Replace data in dayjs object, make sure to provide fallbacks.
dayjs["sameElse"] = DATE_FORMATS[date] ?? DATE_FORMATS.traditional;
dayjs["timeFormat"] = twelvehour ? "hh:mm A" : "HH:mm"; dayjs["timeFormat"] = twelvehour ? "hh:mm A" : "HH:mm";
// Replace {{time}} format string in dayjs strings with the time format.
Object.keys(dayjs) Object.keys(dayjs)
.filter((k) => typeof dayjs[k] === "string") .filter((k) => typeof dayjs[k] === "string")
.forEach( .forEach(
...@@ -180,35 +218,49 @@ function Locale({ children, locale }: Props) { ...@@ -180,35 +218,49 @@ function Locale({ children, locale }: Props) {
return obj; return obj;
} }
useEffect(() => { const loadLanguage = useCallback(
if (locale === "en") { (locale: string) => {
const defn = transformLanguage(definition); if (locale === "en") {
setDefinition(defn); // If English, make sure to restore everything to defaults.
dayjs.locale("en"); // Use what we already have.
dayjs.updateLocale("en", { calendar: defn.dayjs }); const defn = transformLanguage(definition as Dictionary);
return;
}
import(`../../external/lang/${lang.i18n}.json`).then(
async (lang_file) => {
const defn = transformLanguage(lang_file.default);
const target = lang.dayjs ?? lang.i18n;
const dayjs_locale = await import(
`../../node_modules/dayjs/esm/locale/${target}.js`
);
dayjs.locale(target, dayjs_locale.default);
if (defn.dayjs) {
dayjs.updateLocale(target, { calendar: defn.dayjs });
}
setDefinition(defn); setDefinition(defn);
}, dayjs.locale("en");
); dayjs.updateLocale("en", { calendar: defn.dayjs });
}, [locale, lang]); return;
}
import(`../../external/lang/${lang.i18n}.json`).then(
async (lang_file) => {
// Transform the definitions data.
const defn = transformLanguage(lang_file.default);
// Determine and load dayjs locales.
const target = lang.dayjs ?? lang.i18n;
const dayjs_locale = await import(
`../../node_modules/dayjs/esm/locale/${target}.js`
);
// Load dayjs locales.
dayjs.locale(target, dayjs_locale.default);
if (defn.dayjs) {
// Override dayjs calendar locales with our own.
dayjs.updateLocale(target, { calendar: defn.dayjs });
}
// Apply definition to app.
setDefinition(defn);
},
);
},
[lang.dayjs, lang.i18n],
);
useEffect(() => loadLanguage(locale), [locale, lang, loadLanguage]);
useEffect(() => { useEffect(() => {
// Apply RTL language format.
document.body.style.direction = lang.rtl ? "rtl" : ""; document.body.style.direction = lang.rtl ? "rtl" : "";
}, [lang.rtl]); }, [lang.rtl]);
......
...@@ -4,8 +4,6 @@ import { createGlobalStyle } from "styled-components"; ...@@ -4,8 +4,6 @@ import { createGlobalStyle } from "styled-components";
import { createContext } from "preact"; import { createContext } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
...@@ -57,7 +55,7 @@ export type Fonts = ...@@ -57,7 +55,7 @@ export type Fonts =
| "Raleway" | "Raleway"
| "Ubuntu" | "Ubuntu"
| "Comic Neue"; | "Comic Neue";
export type MonoscapeFonts = export type MonospaceFonts =
| "Fira Code" | "Fira Code"
| "Roboto Mono" | "Roboto Mono"
| "Source Code Pro" | "Source Code Pro"
...@@ -70,7 +68,7 @@ export type Theme = { ...@@ -70,7 +68,7 @@ export type Theme = {
light?: boolean; light?: boolean;
font?: Fonts; font?: Fonts;
css?: string; css?: string;
monoscapeFont?: MonoscapeFonts; monospaceFont?: MonospaceFonts;
}; };
export interface ThemeOptions { export interface ThemeOptions {
...@@ -190,8 +188,8 @@ export const FONTS: Record<Fonts, { name: string; load: () => void }> = { ...@@ -190,8 +188,8 @@ export const FONTS: Record<Fonts, { name: string; load: () => void }> = {
}, },
}; };
export const MONOSCAPE_FONTS: Record< export const MONOSPACE_FONTS: Record<
MonoscapeFonts, MonospaceFonts,
{ name: string; load: () => void } { name: string; load: () => void }
> = { > = {
"Fira Code": { "Fira Code": {
...@@ -217,7 +215,7 @@ export const MONOSCAPE_FONTS: Record< ...@@ -217,7 +215,7 @@ export const MONOSCAPE_FONTS: Record<
}; };
export const FONT_KEYS = Object.keys(FONTS).sort(); export const FONT_KEYS = Object.keys(FONTS).sort();
export const MONOSCAPE_FONT_KEYS = Object.keys(MONOSCAPE_FONTS).sort(); export const MONOSPACE_FONT_KEYS = Object.keys(MONOSPACE_FONTS).sort();
export const DEFAULT_FONT = "Open Sans"; export const DEFAULT_FONT = "Open Sans";
export const DEFAULT_MONO_FONT = "Fira Code"; export const DEFAULT_MONO_FONT = "Fira Code";
...@@ -311,17 +309,17 @@ function Theme({ children, options }: Props) { ...@@ -311,17 +309,17 @@ function Theme({ children, options }: Props) {
const font = theme.font ?? DEFAULT_FONT; const font = theme.font ?? DEFAULT_FONT;
root.setProperty("--font", `"${font}"`); root.setProperty("--font", `"${font}"`);
FONTS[font].load(); FONTS[font].load();
}, [theme.font]); }, [root, theme.font]);
useEffect(() => { useEffect(() => {
const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT; const font = theme.monospaceFont ?? DEFAULT_MONO_FONT;
root.setProperty("--monoscape-font", `"${font}"`); root.setProperty("--monospace-font", `"${font}"`);
MONOSCAPE_FONTS[font].load(); MONOSPACE_FONTS[font].load();
}, [theme.monoscapeFont]); }, [root, theme.monospaceFont]);
useEffect(() => { useEffect(() => {
root.setProperty("--ligatures", options?.ligatures ? "normal" : "none"); root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
}, [options?.ligatures]); }, [root, options?.ligatures]);
useEffect(() => { useEffect(() => {
const resize = () => const resize = () =>
...@@ -330,7 +328,7 @@ function Theme({ children, options }: Props) { ...@@ -330,7 +328,7 @@ function Theme({ children, options }: Props) {
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize); return () => window.removeEventListener("resize", resize);
}, []); }, [root]);
return ( return (
<ThemeContext.Provider value={theme}> <ThemeContext.Provider value={theme}>
......
import { Channel } from "revolt.js/dist/maps/Channels";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "preact/hooks";
import type { ProduceType, VoiceUser } from "../lib/vortex/Types"; import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
import type VoiceClient from "../lib/vortex/VoiceClient"; import type VoiceClient from "../lib/vortex/VoiceClient";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import { SoundContext } from "./Settings"; import { SoundContext } from "./Settings";
import { AppContext } from "./revoltjs/RevoltClient";
import { useForceUpdate } from "./revoltjs/hooks";
export enum VoiceStatus { export enum VoiceStatus {
LOADING = 0, LOADING = 0,
...@@ -22,7 +29,7 @@ export enum VoiceStatus { ...@@ -22,7 +29,7 @@ export enum VoiceStatus {
} }
export interface VoiceOperations { export interface VoiceOperations {
connect: (channelId: string) => Promise<void>; connect: (channel: Channel) => Promise<Channel>;
disconnect: () => void; disconnect: () => void;
isProducing: (type: ProduceType) => boolean; isProducing: (type: ProduceType) => boolean;
startProducing: (type: ProduceType) => Promise<void>; startProducing: (type: ProduceType) => Promise<void>;
...@@ -44,20 +51,22 @@ type Props = { ...@@ -44,20 +51,22 @@ type Props = {
}; };
export default function Voice({ children }: Props) { export default function Voice({ children }: Props) {
const revoltClient = useContext(AppContext);
const [client, setClient] = useState<VoiceClient | undefined>(undefined); const [client, setClient] = useState<VoiceClient | undefined>(undefined);
const [state, setState] = useState<VoiceState>({ const [state, setState] = useState<VoiceState>({
status: VoiceStatus.LOADING, status: VoiceStatus.LOADING,
participants: new Map(), participants: new Map(),
}); });
function setStatus(status: VoiceStatus, roomId?: string) { const setStatus = useCallback(
setState({ (status: VoiceStatus, roomId?: string) => {
status, setState({
roomId: roomId ?? client?.roomId, status,
participants: client?.participants ?? new Map(), roomId: roomId ?? client?.roomId,
}); participants: client?.participants ?? new Map(),
} });
},
[client?.participants, client?.roomId],
);
useEffect(() => { useEffect(() => {
import("../lib/vortex/VoiceClient") import("../lib/vortex/VoiceClient")
...@@ -75,32 +84,30 @@ export default function Voice({ children }: Props) { ...@@ -75,32 +84,30 @@ export default function Voice({ children }: Props) {
console.error("Failed to load voice library!", err); console.error("Failed to load voice library!", err);
setStatus(VoiceStatus.UNAVAILABLE); setStatus(VoiceStatus.UNAVAILABLE);
}); });
}, []); }, [setStatus]);
const isConnecting = useRef(false); const isConnecting = useRef(false);
const operations: VoiceOperations = useMemo(() => { const operations: VoiceOperations = useMemo(() => {
return { return {
connect: async (channelId) => { connect: async (channel) => {
if (!client?.supported()) throw new Error("RTC is unavailable"); if (!client?.supported()) throw new Error("RTC is unavailable");
isConnecting.current = true; isConnecting.current = true;
setStatus(VoiceStatus.CONNECTING, channelId); setStatus(VoiceStatus.CONNECTING, channel._id);
try { try {
const call = await revoltClient.channels.joinCall( const call = await channel.joinCall();
channelId,
);
if (!isConnecting.current) { if (!isConnecting.current) {
setStatus(VoiceStatus.READY); setStatus(VoiceStatus.READY);
return; return channel;
} }
// ! FIXME: use configuration to check if voso is enabled // ! TODO: use configuration to check if voso is enabled
// await client.connect("wss://voso.revolt.chat/ws"); // await client.connect("wss://voso.revolt.chat/ws");
await client.connect( await client.connect(
"wss://voso.revolt.chat/ws", "wss://voso.revolt.chat/ws",
channelId, channel._id,
); );
setStatus(VoiceStatus.AUTHENTICATING); setStatus(VoiceStatus.AUTHENTICATING);
...@@ -112,11 +119,12 @@ export default function Voice({ children }: Props) { ...@@ -112,11 +119,12 @@ export default function Voice({ children }: Props) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setStatus(VoiceStatus.READY); setStatus(VoiceStatus.READY);
return; return channel;
} }
setStatus(VoiceStatus.CONNECTED); setStatus(VoiceStatus.CONNECTED);
isConnecting.current = false; isConnecting.current = false;
return channel;
}, },
disconnect: () => { disconnect: () => {
if (!client?.supported()) throw new Error("RTC is unavailable"); if (!client?.supported()) throw new Error("RTC is unavailable");
...@@ -138,9 +146,9 @@ export default function Voice({ children }: Props) { ...@@ -138,9 +146,9 @@ export default function Voice({ children }: Props) {
switch (type) { switch (type) {
case "audio": { case "audio": {
if (client?.audioProducer !== undefined) if (client?.audioProducer !== undefined)
return console.log("No audio producer."); // ! FIXME: let the user know return console.log("No audio producer."); // ! TODO: let the user know
if (navigator.mediaDevices === undefined) if (navigator.mediaDevices === undefined)
return console.log("No media devices."); // ! FIXME: let the user know return console.log("No media devices."); // ! TODO: let the user know
const mediaStream = const mediaStream =
await navigator.mediaDevices.getUserMedia({ await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
...@@ -158,44 +166,49 @@ export default function Voice({ children }: Props) { ...@@ -158,44 +166,49 @@ export default function Voice({ children }: Props) {
return client?.stopProduce(type); return client?.stopProduce(type);
}, },
}; };
}, [client]); }, [client, setStatus]);
const { forceUpdate } = useForceUpdate();
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
useEffect(() => { useEffect(() => {
if (!client?.supported()) return; if (!client?.supported()) return;
// ! FIXME: message for fatal: // ! TODO: message for fatal:
// ! get rid of these force updates // ! get rid of these force updates
// ! handle it through state or smth // ! handle it through state or smth
client.on("startProduce", forceUpdate); function stateUpdate() {
client.on("stopProduce", forceUpdate); setStatus(state.status);
}
client.on("startProduce", stateUpdate);
client.on("stopProduce", stateUpdate);
client.on("userJoined", () => { client.on("userJoined", () => {
playSound("call_join"); playSound("call_join");
forceUpdate(); stateUpdate();
}); });
client.on("userLeft", () => { client.on("userLeft", () => {
playSound("call_leave"); playSound("call_leave");
forceUpdate(); stateUpdate();
}); });
client.on("userStartProduce", forceUpdate);
client.on("userStopProduce", forceUpdate); client.on("userStartProduce", stateUpdate);
client.on("close", forceUpdate); client.on("userStopProduce", stateUpdate);
client.on("close", stateUpdate);
return () => { return () => {
client.removeListener("startProduce", forceUpdate); client.removeListener("startProduce", stateUpdate);
client.removeListener("stopProduce", forceUpdate); client.removeListener("stopProduce", stateUpdate);
client.removeListener("userJoined", forceUpdate); client.removeListener("userJoined", stateUpdate);
client.removeListener("userLeft", forceUpdate); client.removeListener("userLeft", stateUpdate);
client.removeListener("userStartProduce", forceUpdate); client.removeListener("userStartProduce", stateUpdate);
client.removeListener("userStopProduce", forceUpdate); client.removeListener("userStopProduce", stateUpdate);
client.removeListener("close", forceUpdate); client.removeListener("close", stateUpdate);
}; };
}, [client, state]); }, [client, state, playSound, setStatus]);
return ( return (
<VoiceContext.Provider value={state}> <VoiceContext.Provider value={state}>
......
...@@ -12,12 +12,12 @@ import { SignedOutModal } from "./modals/SignedOut"; ...@@ -12,12 +12,12 @@ import { SignedOutModal } from "./modals/SignedOut";
export interface Props { export interface Props {
screen: Screen; screen: Screen;
openScreen: (id: any) => void; openScreen: (screen: Screen) => void;
} }
export default function Modals({ screen, openScreen }: Props) { export default function Modals({ screen, openScreen }: Props) {
const onClose = () => const onClose = () =>
isModalClosing isModalClosing || screen.id === "onboarding"
? openScreen({ id: "none" }) ? openScreen({ id: "none" })
: internalEmit("Modal", "close"); : internalEmit("Modal", "close");
......