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 536 additions and 320 deletions
import { X, Crown } from "@styled-icons/boxicons-regular"; import { X, Crown } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channels, Users } from "revolt.js/dist/api/objects"; 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";
...@@ -10,8 +12,6 @@ import { Localizer, Text } from "preact-i18n"; ...@@ -10,8 +12,6 @@ import { Localizer, Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { stopPropagation } from "../../../lib/stopPropagation"; import { stopPropagation } from "../../../lib/stopPropagation";
import { Channel, User } from "../../../mobx";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import ChannelIcon from "../../common/ChannelIcon"; import ChannelIcon from "../../common/ChannelIcon";
...@@ -51,8 +51,7 @@ export const UserButton = observer((props: UserProps) => { ...@@ -51,8 +51,7 @@ export const UserButton = observer((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,
...@@ -82,7 +81,7 @@ export const UserButton = observer((props: UserProps) => { ...@@ -82,7 +81,7 @@ export const UserButton = observer((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" />}>
...@@ -137,7 +136,7 @@ export const ChannelButton = observer((props: ChannelProps) => { ...@@ -137,7 +136,7 @@ export const ChannelButton = observer((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,
......
...@@ -6,8 +6,7 @@ import { ...@@ -6,8 +6,7 @@ import {
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; 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";
...@@ -16,14 +15,12 @@ import ConditionalLink from "../../../lib/ConditionalLink"; ...@@ -16,14 +15,12 @@ import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useData } from "../../../mobx/State";
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 { 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 } 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";
...@@ -43,8 +40,7 @@ const HomeSidebar = observer((props: Props) => { ...@@ -43,8 +40,7 @@ const HomeSidebar = observer((props: Props) => {
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const store = useData(); const channels = [...client.channels.values()]
const channels = [...store.channels.values()]
.filter( .filter(
(x) => (x) =>
x.channel_type === "DirectMessage" || x.channel_type === "DirectMessage" ||
...@@ -52,7 +48,7 @@ const HomeSidebar = observer((props: Props) => { ...@@ -52,7 +48,7 @@ const HomeSidebar = observer((props: Props) => {
) )
.map((x) => mapChannelWithUnread(x, props.unreads)); .map((x) => mapChannelWithUnread(x, props.unreads));
const obj = store.channels.get(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 });
...@@ -88,10 +84,10 @@ const HomeSidebar = observer((props: Props) => { ...@@ -88,10 +84,10 @@ const HomeSidebar = observer((props: Props) => {
<ButtonItem <ButtonItem
active={pathname === "/friends"} active={pathname === "/friends"}
alert={ alert={
typeof [...store.users.values()].find( typeof [...client.users.values()].find(
(user) => (user) =>
user?.relationship === user?.relationship ===
UsersNS.Relationship.Incoming, RelationshipStatus.Incoming,
) !== "undefined" ) !== "undefined"
? "unread" ? "unread"
: undefined : undefined
...@@ -140,11 +136,7 @@ const HomeSidebar = observer((props: Props) => { ...@@ -140,11 +136,7 @@ const HomeSidebar = observer((props: Props) => {
let user; let user;
if (x.channel.channel_type === "DirectMessage") { if (x.channel.channel_type === "DirectMessage") {
if (!x.channel.active) return null; if (!x.channel.active) return null;
user = x.channel.recipient;
const recipient = client.channels.getRecipient(
x.channel._id,
);
user = store.users.get(recipient);
if (!user) { if (!user) {
console.warn( console.warn(
...@@ -156,6 +148,7 @@ const HomeSidebar = observer((props: Props) => { ...@@ -156,6 +148,7 @@ const HomeSidebar = observer((props: Props) => {
return ( return (
<ConditionalLink <ConditionalLink
key={x.channel._id}
active={x.channel._id === channel} active={x.channel._id === channel}
to={`/channel/${x.channel._id}`}> to={`/channel/${x.channel._id}`}>
<ChannelButton <ChannelButton
......
import { Plus } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useLocation, useParams } from "react-router-dom"; import { useHistory, useLocation, useParams } from "react-router-dom";
import { Channel, Servers, Users } from "revolt.js/dist/api/objects"; 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";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useData } from "../../../mobx/State";
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 { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import { useForceUpdate, 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";
...@@ -63,6 +60,7 @@ function Icon({ ...@@ -63,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;
...@@ -76,7 +74,8 @@ const ServerList = styled.div` ...@@ -76,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;
...@@ -88,6 +87,11 @@ const ServerList = styled.div` ...@@ -88,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 }>`
...@@ -95,9 +99,13 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -95,9 +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: 10px; padding-inline-start: 6px;
display: grid; display: grid;
place-items: center; place-items: center;
...@@ -130,8 +138,6 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -130,8 +138,6 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
svg { svg {
margin-top: 5px; margin-top: 5px;
display: relative;
pointer-events: none; pointer-events: none;
// outline: 1px solid red; // outline: 1px solid red;
} }
...@@ -148,21 +154,20 @@ function Swoosh() { ...@@ -148,21 +154,20 @@ function Swoosh() {
return ( return (
<span> <span>
<svg <svg
width="56" width="54"
height="103" height="106"
viewBox="0 0 56 103" viewBox="0 0 54 106"
fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path <path
d="M55.0368 51.5947C55.0368 64.8596 44.2834 75.613 31.0184 75.613C17.7534 75.613 7 64.8596 7 51.5947C7 38.3297 17.7534 27.5763 31.0184 27.5763C44.2834 27.5763 55.0368 38.3297 55.0368 51.5947Z" 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"
fill="var(--sidebar-active)" fill="var(--sidebar-active)"
/> />
<path <path
d="M55.8809 1C55.5597 16.9971 34.4597 25.2244 24.0847 28.6715L55.8846 60.4859L55.8809 1Z" d="M27 80C4.5 80 54 53 54 53L54.0001 106C54.0001 106 49.5 80 27 80Z"
fill="var(--sidebar-active)" fill="var(--sidebar-active)"
/> />
<path <path
d="M55.8809 102.249C55.5597 86.2516 34.4597 78.0243 24.0847 74.5771L55.8846 42.7627L55.8809 102.249Z" d="M27 26C4.5 26 54 53 54 53L53.9999 0C53.9999 0 49.5 26 27 26Z"
fill="var(--sidebar-active)" fill="var(--sidebar-active)"
/> />
</svg> </svg>
...@@ -176,13 +181,12 @@ interface Props { ...@@ -176,13 +181,12 @@ interface Props {
} }
export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
const store = useData();
const client = useClient(); const client = useClient();
const self = store.users.get(client.user!._id);
const ctx = useForceUpdate(); const { server: server_id } = useParams<{ server?: string }>();
const activeServers = useServers(undefined, ctx) as Servers.Server[]; const server = server_id ? client.servers.get(server_id) : undefined;
const channels = [...store.channels.values()].map((x) => const activeServers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) =>
mapChannelWithUnread(x, unreads), mapChannelWithUnread(x, unreads),
); );
...@@ -192,7 +196,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -192,7 +196,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
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.channel?._id === id); const channel = channels.find((x) => x.channel?._id === id);
if (channel?.alertCount) { if (channel?.alertCount) {
alertCount += channel.alertCount; alertCount += channel.alertCount;
...@@ -200,8 +204,8 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -200,8 +204,8 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
} }
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
...@@ -212,10 +216,8 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -212,10 +216,8 @@ export const ServerListSidebar = observer(({ 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;
...@@ -224,7 +226,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -224,7 +226,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
if ( if (
(x.channel?.channel_type === "DirectMessage" (x.channel?.channel_type === "DirectMessage"
? x.channel?.active ? x.channel?.active
: true) && : x.channel?.channel_type === "Group") &&
x.unread x.unread
) { ) {
homeUnread = "unread"; homeUnread = "unread";
...@@ -233,8 +235,8 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -233,8 +235,8 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
} }
if ( if (
[...store.users.values()].find( [...client.users.values()].find(
(x) => x.relationship === Users.Relationship.Incoming, (x) => x.relationship === RelationshipStatus.Incoming,
) )
) { ) {
alertCount++; alertCount++;
...@@ -255,11 +257,16 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -255,11 +257,16 @@ export const ServerListSidebar = observer(({ 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 target={self} size={32} status /> <UserIcon
target={client.user}
size={32}
status
hover
/>
</Icon> </Icon>
</UserHover> </UserHover>
</div> </div>
...@@ -267,24 +274,30 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -267,24 +274,30 @@ export const ServerListSidebar = observer(({ 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>
...@@ -300,6 +313,15 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { ...@@ -300,6 +313,15 @@ export const ServerListSidebar = observer(({ 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>
......
import { observer } from "mobx-react-lite"; 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 { useData } from "../../../mobx/State";
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 { useForceUpdate, useServer } from "../../../context/revoltjs/hooks"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader"; import ServerHeader from "../../common/ServerHeader";
...@@ -35,10 +34,13 @@ const ServerBase = styled.div` ...@@ -35,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`
...@@ -52,18 +54,16 @@ const ServerList = styled.div` ...@@ -52,18 +54,16 @@ const ServerList = styled.div`
`; `;
const ServerSidebar = observer((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 store = useData(); const channel = channel_id ? client.channels.get(channel_id) : undefined;
const channel = channel_id ? store.channels.get(channel_id) : undefined;
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;
...@@ -73,13 +73,13 @@ const ServerSidebar = observer((props: Props) => { ...@@ -73,13 +73,13 @@ const ServerSidebar = observer((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 = store.channels.get(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;
...@@ -125,7 +125,7 @@ const ServerSidebar = observer((props: Props) => { ...@@ -125,7 +125,7 @@ const ServerSidebar = observer((props: Props) => {
return ( return (
<ServerBase> <ServerBase>
<ServerHeader server={server} ctx={ctx} /> <ServerHeader server={server} />
<ConnectionStatus /> <ConnectionStatus />
<ServerList <ServerList
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
......
import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect } from "preact/hooks"; import { useLayoutEffect } from "preact/hooks";
import { Channel } from "../../../mobx";
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 (
...@@ -40,19 +35,16 @@ export function useUnreads( ...@@ -40,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]);
} }
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useRenderState } from "../../../lib/renderer/Singleton"; import { useRenderState } from "../../../lib/renderer/Singleton";
interface Props { interface Props {
......
/* eslint-disable react-hooks/rules-of-hooks */
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router"; import { Link, useParams } from "react-router-dom";
import { Link } from "react-router-dom"; import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Channels, Message, Servers, Users } from "revolt.js/dist/api/objects"; import { Message } from "revolt.js/dist/maps/Messages";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { Channel } from "../../../mobx";
import { useData } from "../../../mobx/State";
import { getState } from "../../../redux"; 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 } from "../../../context/revoltjs/hooks";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import Button from "../../ui/Button"; import Button from "../../ui/Button";
...@@ -31,7 +28,11 @@ import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; ...@@ -31,7 +28,11 @@ import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import { UserButton } from "../items/ButtonItem"; import { UserButton } from "../items/ButtonItem";
import { ChannelDebugInfo } from "./ChannelDebugInfo"; import { ChannelDebugInfo } from "./ChannelDebugInfo";
export default function MemberSidebar({ channel }: { channel?: Channel }) { export default function MemberSidebar({ channel: obj }: { channel?: Channel }) {
const { channel: channel_id } = useParams<{ channel: string }>();
const client = useClient();
const channel = obj ?? client.channels.get(channel_id);
switch (channel?.channel_type) { switch (channel?.channel_type) {
case "Group": case "Group":
return <GroupMemberSidebar channel={channel} />; return <GroupMemberSidebar channel={channel} />;
...@@ -46,10 +47,9 @@ export const GroupMemberSidebar = observer( ...@@ -46,10 +47,9 @@ export const GroupMemberSidebar = observer(
({ channel }: { channel: Channel }) => { ({ channel }: { channel: Channel }) => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const store = useData(); const members = channel.recipients?.filter(
const members = channel.recipients (x) => typeof x !== "undefined",
?.map((member) => store.users.get(member)!) );
.filter((x) => typeof x !== "undefined");
/*const voice = useContext(VoiceContext); /*const voice = useContext(VoiceContext);
const voiceActive = voice.roomId === channel._id; const voiceActive = voice.roomId === channel._id;
...@@ -70,14 +70,12 @@ export const GroupMemberSidebar = observer( ...@@ -70,14 +70,12 @@ export const GroupMemberSidebar = observer(
// ! FIXME: should probably rewrite all this code // ! FIXME: should probably rewrite all this code
const l = const l =
+( +(
(a.online && (a!.online && a!.status?.presence !== Presence.Invisible) ??
a.status?.presence !== Users.Presence.Invisible) ??
false false
) | 0; ) | 0;
const r = const r =
+( +(
(b.online && (b!.online && b!.status?.presence !== Presence.Invisible) ??
b.status?.presence !== Users.Presence.Invisible) ??
false false
) | 0; ) | 0;
...@@ -86,14 +84,14 @@ export const GroupMemberSidebar = observer( ...@@ -86,14 +84,14 @@ export const GroupMemberSidebar = observer(
return n; return n;
} }
return a.username.localeCompare(b.username); return a!.username.localeCompare(b!.username);
}); });
return ( return (
<GenericSidebarBase> <GenericSidebarBase>
<GenericSidebarList> <GenericSidebarList>
<ChannelDebugInfo id={channel._id} /> <ChannelDebugInfo id={channel._id} />
<Search channel={channel._id} /> <Search channel={channel} />
{/*voiceActive && voiceParticipants.length !== 0 && ( {/*voiceActive && voiceParticipants.length !== 0 && (
<Fragment> <Fragment>
...@@ -163,71 +161,33 @@ export const GroupMemberSidebar = observer( ...@@ -163,71 +161,33 @@ export const GroupMemberSidebar = observer(
export const ServerMemberSidebar = observer( export const ServerMemberSidebar = observer(
({ channel }: { channel: Channel }) => { ({ channel }: { channel: Channel }) => {
const [members, setMembers] = useState<Servers.Member[] | undefined>( const client = useClient();
undefined,
);
const store = useData();
const users = members
?.map((member) => store.users.get(member._id.user)!)
.filter((x) => typeof x !== "undefined");
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const status = useContext(StatusContext); const status = useContext(StatusContext);
const client = useContext(AppContext);
useEffect(() => { useEffect(() => {
if ( if (status === ClientStatus.ONLINE) {
status === ClientStatus.ONLINE && channel.server!.fetchMembers();
typeof members === "undefined"
) {
client.members
.fetchMembers(channel.server!)
.then((members) => setMembers(members));
} }
}, [status]); }, [status, channel.server]);
// ! FIXME: temporary code const users = [...client.members.keys()]
useEffect(() => { .map((x) => JSON.parse(x))
function onPacket(packet: ClientboundNotification) { .filter((x) => x.server === channel.server_id)
if (!members) return; .map((y) => client.users.get(y.user)!)
if (packet.type === "ServerMemberJoin") { .filter((z) => typeof z !== "undefined");
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 // copy paste from above
users?.sort((a, b) => { users.sort((a, b) => {
// ! FIXME: should probably rewrite all this code // ! FIXME: should probably rewrite all this code
const l = const l =
+( +(
(a.online && (a.online && a.status?.presence !== Presence.Invisible) ??
a.status?.presence !== Users.Presence.Invisible) ??
false false
) | 0; ) | 0;
const r = const r =
+( +(
(b.online && (b.online && b.status?.presence !== Presence.Invisible) ??
b.status?.presence !== Users.Presence.Invisible) ??
false false
) | 0; ) | 0;
...@@ -243,9 +203,9 @@ export const ServerMemberSidebar = observer( ...@@ -243,9 +203,9 @@ export const ServerMemberSidebar = observer(
<GenericSidebarBase> <GenericSidebarBase>
<GenericSidebarList> <GenericSidebarList>
<ChannelDebugInfo id={channel._id} /> <ChannelDebugInfo id={channel._id} />
<Search channel={channel._id} /> <Search channel={channel} />
<div>{!members && <Preloader type="ring" />}</div> <div>{users.length === 0 && <Preloader type="ring" />}</div>
{members && ( {users.length > 0 && (
<CollapsibleSection <CollapsibleSection
//sticky //will re-add later, need to fix css //sticky //will re-add later, need to fix css
id="members" id="members"
...@@ -256,10 +216,7 @@ export const ServerMemberSidebar = observer( ...@@ -256,10 +216,7 @@ export const ServerMemberSidebar = observer(
{users?.length ?? 0} {users?.length ?? 0}
</span> </span>
}> }>
{(users?.length ?? 0) === 0 && ( {users.map(
<img src={placeholderSVG} loading="eager" />
)}
{users?.map(
(user) => (user) =>
user && ( user && (
<UserButton <UserButton
...@@ -283,10 +240,9 @@ export const ServerMemberSidebar = observer( ...@@ -283,10 +240,9 @@ export const ServerMemberSidebar = observer(
}, },
); );
function Search({ channel }: { channel: string }) { 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");
...@@ -294,11 +250,7 @@ function Search({ channel }: { channel: string }) { ...@@ -294,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);
} }
...@@ -315,6 +267,7 @@ function Search({ channel }: { channel: string }) { ...@@ -315,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}
...@@ -340,25 +293,21 @@ function Search({ channel }: { channel: string }) { ...@@ -340,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 &&
......
...@@ -64,7 +64,7 @@ export default function Tip( ...@@ -64,7 +64,7 @@ export default function Tip(
{!hideSeparator && <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";
...@@ -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.monospaceFont ?? DEFAULT_MONO_FONT; const font = theme.monospaceFont ?? DEFAULT_MONO_FONT;
root.setProperty("--monospace-font", `"${font}"`); root.setProperty("--monospace-font", `"${font}"`);
MONOSPACE_FONTS[font].load(); MONOSPACE_FONTS[font].load();
}, [theme.monospaceFont]); }, [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";
export enum VoiceStatus { export enum VoiceStatus {
LOADING = 0, LOADING = 0,
...@@ -21,7 +29,7 @@ export enum VoiceStatus { ...@@ -21,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>;
...@@ -43,20 +51,22 @@ type Props = { ...@@ -43,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")
...@@ -74,32 +84,30 @@ export default function Voice({ children }: Props) { ...@@ -74,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);
...@@ -111,11 +119,12 @@ export default function Voice({ children }: Props) { ...@@ -111,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");
...@@ -137,9 +146,9 @@ export default function Voice({ children }: Props) { ...@@ -137,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,
...@@ -157,14 +166,14 @@ export default function Voice({ children }: Props) { ...@@ -157,14 +166,14 @@ export default function Voice({ children }: Props) {
return client?.stopProduce(type); return client?.stopProduce(type);
}, },
}; };
}, [client]); }, [client, setStatus]);
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
...@@ -199,7 +208,7 @@ export default function Voice({ children }: Props) { ...@@ -199,7 +208,7 @@ export default function Voice({ children }: Props) {
client.removeListener("userStopProduce", stateUpdate); client.removeListener("userStopProduce", stateUpdate);
client.removeListener("close", stateUpdate); client.removeListener("close", stateUpdate);
}; };
}, [client, state]); }, [client, state, playSound, setStatus]);
return ( return (
<VoiceContext.Provider value={state}> <VoiceContext.Provider value={state}>
......
...@@ -2,7 +2,6 @@ import { BrowserRouter as Router } from "react-router-dom"; ...@@ -2,7 +2,6 @@ import { BrowserRouter as Router } from "react-router-dom";
import State from "../redux/State"; import State from "../redux/State";
import MobXState from "../mobx/State";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import Locale from "./Locale"; import Locale from "./Locale";
import Settings from "./Settings"; import Settings from "./Settings";
...@@ -15,19 +14,17 @@ export default function Context({ children }: { children: Children }) { ...@@ -15,19 +14,17 @@ export default function Context({ children }: { children: Children }) {
return ( return (
<Router> <Router>
<State> <State>
<MobXState> <Theme>
<Theme> <Settings>
<Settings> <Locale>
<Locale> <Intermediate>
<Intermediate> <Client>
<Client> <Voice>{children}</Voice>
<Voice>{children}</Voice> </Client>
</Client> </Intermediate>
</Intermediate> </Locale>
</Locale> </Settings>
</Settings> </Theme>
</Theme>
</MobXState>
</State> </State>
</Router> </Router>
); );
......
import { Prompt } from "react-router"; import { Prompt } from "react-router";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import type { Attachment } from "revolt-api/types/Autumn";
Attachment, import type { EmbedImage } from "revolt-api/types/January";
Channels, import { Channel } from "revolt.js/dist/maps/Channels";
EmbedImage, import { Message } from "revolt.js/dist/maps/Messages";
Servers, import { Server } from "revolt.js/dist/maps/Servers";
Users, import { User } from "revolt.js/dist/maps/Users";
} from "revolt.js/dist/api/objects";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { internalSubscribe } from "../../lib/eventEmitter"; import { internalSubscribe } from "../../lib/eventEmitter";
import { Channel, User } from "../../mobx";
import { Action } from "../../components/ui/Modal"; import { Action } from "../../components/ui/Modal";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
...@@ -36,19 +33,19 @@ export type Screen = ...@@ -36,19 +33,19 @@ export type Screen =
| ({ id: "special_prompt" } & ( | ({ id: "special_prompt" } & (
| { type: "leave_group"; target: Channel } | { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channel } | { type: "close_dm"; target: Channel }
| { type: "leave_server"; target: Servers.Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Servers.Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: Channels.Message } | { type: "delete_message"; target: Message }
| { | {
type: "create_invite"; type: "create_invite";
target: Channel; target: Channel;
} }
| { type: "kick_member"; target: Servers.Server; user: User } | { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Servers.Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Servers.Server } | { type: "create_channel"; target: Server }
)) ))
| ({ id: "special_input" } & ( | ({ id: "special_input" } & (
| { | {
...@@ -60,7 +57,7 @@ export type Screen = ...@@ -60,7 +57,7 @@ export type Screen =
} }
| { | {
type: "create_role"; type: "create_role";
server: string; server: Server;
callback: (id: string) => void; callback: (id: string) => void;
} }
)) ))
...@@ -92,13 +89,16 @@ export type Screen = ...@@ -92,13 +89,16 @@ export type Screen =
}; };
export const IntermediateContext = createContext({ export const IntermediateContext = createContext({
screen: { id: "none" } as Screen, screen: { id: "none" },
focusTaken: false, focusTaken: false,
}); });
export const IntermediateActionsContext = createContext({ export const IntermediateActionsContext = createContext<{
openScreen: (screen: Screen) => {}, openScreen: (screen: Screen) => void;
writeClipboard: (text: string) => {}, writeClipboard: (text: string) => void;
}>({
openScreen: null!,
writeClipboard: null!,
}); });
interface Props { interface Props {
...@@ -133,12 +133,20 @@ export default function Intermediate(props: Props) { ...@@ -133,12 +133,20 @@ export default function Intermediate(props: Props) {
const navigate = (path: string) => history.push(path); const navigate = (path: string) => history.push(path);
const subs = [ const subs = [
internalSubscribe("Intermediate", "openProfile", openProfile), internalSubscribe(
internalSubscribe("Intermediate", "navigate", navigate), "Intermediate",
"openProfile",
openProfile as (...args: unknown[]) => void,
),
internalSubscribe(
"Intermediate",
"navigate",
navigate as (...args: unknown[]) => void,
),
]; ];
return () => subs.map((unsub) => unsub()); return () => subs.map((unsub) => unsub());
}, []); }, [history]);
return ( return (
<IntermediateContext.Provider value={value}> <IntermediateContext.Provider value={value}>
......
...@@ -12,7 +12,7 @@ import { SignedOutModal } from "./modals/SignedOut"; ...@@ -12,7 +12,7 @@ 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) {
......
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -81,7 +82,7 @@ type SpecialProps = { onClose: () => void } & ( ...@@ -81,7 +82,7 @@ type SpecialProps = { onClose: () => void } & (
| "set_custom_status" | "set_custom_status"
| "add_friend"; | "add_friend";
} }
| { type: "create_role"; server: string; callback: (id: string) => void } | { type: "create_role"; server: Server; callback: (id: string) => void }
); );
export function SpecialInputModal(props: SpecialProps) { export function SpecialInputModal(props: SpecialProps) {
...@@ -134,10 +135,7 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -134,10 +135,7 @@ export function SpecialInputModal(props: SpecialProps) {
} }
field={<Text id="app.settings.permissions.role_name" />} field={<Text id="app.settings.permissions.role_name" />}
callback={async (name) => { callback={async (name) => {
const role = await client.servers.createRole( const role = await props.server.createRole(name);
props.server,
name,
);
props.callback(role.id); props.callback(role.id);
}} }}
/> />
...@@ -151,7 +149,7 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -151,7 +149,7 @@ export function SpecialInputModal(props: SpecialProps) {
field={<Text id="app.context_menu.custom_status" />} field={<Text id="app.context_menu.custom_status" />}
defaultValue={client.user?.status?.text} defaultValue={client.user?.status?.text}
callback={(text) => callback={(text) =>
client.users.editUser({ client.users.edit({
status: { status: {
...client.user?.status, ...client.user?.status,
text: text.trim().length > 0 ? text : undefined, text: text.trim().length > 0 ? text : undefined,
...@@ -166,7 +164,14 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -166,7 +164,14 @@ export function SpecialInputModal(props: SpecialProps) {
<InputModal <InputModal
onClose={onClose} onClose={onClose}
question={"Add Friend"} question={"Add Friend"}
callback={(username) => client.users.addFriend(username)} callback={(username) =>
client
.req(
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined)
}
/> />
); );
} }
......
...@@ -29,7 +29,7 @@ export function OnboardingModal({ onClose, callback }: Props) { ...@@ -29,7 +29,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
setLoading(true); setLoading(true);
callback(username, true) callback(username, true)
.then(() => onClose()) .then(() => onClose())
.catch((err: any) => { .catch((err: unknown) => {
setError(takeError(err)); setError(takeError(err));
setLoading(false); setLoading(false);
}); });
......