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 590 additions and 463 deletions
import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
import { useContext, useEffect, useState } from "preact/hooks";
......@@ -9,8 +10,6 @@ import {
IntermediateContext,
useIntermediate,
} from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import AutoComplete, {
useAutoComplete,
......@@ -44,7 +43,7 @@ const EditorBase = styled.div`
`;
interface Props {
message: MessageObject;
message: Message;
finish: () => void;
}
......@@ -52,7 +51,6 @@ export default function MessageEditor({ message, finish }: Props) {
const [content, setContent] = useState((message.content as string) ?? "");
const { focusTaken } = useContext(IntermediateContext);
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
async function save() {
finish();
......@@ -60,13 +58,11 @@ export default function MessageEditor({ message, finish }: Props) {
if (content.length === 0) {
openScreen({
id: "special_prompt",
// @ts-expect-error
type: "delete_message",
// @ts-expect-error
target: message,
});
} else if (content !== message.content) {
await client.channels.editMessage(message.channel, message._id, {
await message.edit({
content,
});
}
......@@ -82,7 +78,7 @@ export default function MessageEditor({ message, finish }: Props) {
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken]);
}, [focusTaken, finish]);
const {
onChange,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { X } from "@styled-icons/boxicons-regular";
import { Users } from "revolt.js/dist/api/objects";
import { RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import { memo } from "preact/compat";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
import { RenderState } from "../../../lib/renderer/types";
......@@ -14,8 +17,7 @@ import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import Message from "../../../components/common/messaging/Message";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
......@@ -47,7 +49,7 @@ const BlockedMessage = styled.div`
function MessageRenderer({ id, state, queue, highlight }: Props) {
if (state.type !== "RENDER") return null;
const client = useContext(AppContext);
const client = useClient();
const userId = client.user!._id;
const [editing, setEditing] = useState<string | undefined>(undefined);
......@@ -60,7 +62,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
function editLast() {
if (state.type !== "RENDER") return;
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author === userId) {
if (state.messages[i].author_id === userId) {
setEditing(state.messages[i]._id);
internalEmit("MessageArea", "jump_to_bottom");
return;
......@@ -74,10 +76,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
];
return () => subs.forEach((unsub) => unsub());
}, [state.messages]);
}, [state.messages, state.type, userId]);
let render: Children[] = [],
previous: MessageObject | undefined;
const render: Children[] = [];
let previous: MessageI | undefined;
if (state.atTop) {
render.push(<ConversationStart id={id} />);
......@@ -129,10 +131,15 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
for (const message of state.messages) {
if (previous) {
compare(message._id, message.author, previous._id, previous.author);
compare(
message._id,
message.author_id,
previous._id,
previous.author_id,
);
}
if (message.author === "00000000000000000000000000") {
if (message.author_id === SYSTEM_USER_ID) {
render.push(
<SystemMessage
key={message._id}
......@@ -141,34 +148,30 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
highlight={highlight === message._id}
/>,
);
} else if (
message.author?.relationship === RelationshipStatus.Blocked
) {
blocked++;
} else {
// ! FIXME: temp solution
if (
client.users.get(message.author)?.relationship ===
Users.Relationship.Blocked
) {
blocked++;
} else {
if (blocked > 0) pushBlocked();
render.push(
<Message
message={message}
key={message._id}
head={head}
content={
editing === message._id ? (
<MessageEditor
message={message}
finish={stopEditing}
/>
) : undefined
}
attachContext
highlight={highlight === message._id}
/>,
);
}
if (blocked > 0) pushBlocked();
render.push(
<Message
message={message}
key={message._id}
head={head}
content={
editing === message._id ? (
<MessageEditor
message={message}
finish={stopEditing}
/>
) : undefined
}
attachContext
highlight={highlight === message._id}
/>,
);
}
previous = message;
......@@ -183,20 +186,22 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
if (nonces.includes(msg.id)) continue;
if (previous) {
compare(msg.id, userId!, previous._id, previous.author);
compare(msg.id, userId!, previous._id, previous.author_id);
previous = {
_id: msg.id,
data: { author: userId! },
} as any;
author_id: userId!,
} as MessageI;
}
render.push(
<Message
message={{
...msg.data,
replies: msg.data.replies.map((x) => x.id),
}}
message={
new MessageI(client, {
...msg.data,
replies: msg.data.replies.map((x) => x.id),
})
}
key={msg.id}
queued={msg}
head={head}
......
import { BarChart } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import { Text } from "preact-i18n";
......@@ -9,11 +10,7 @@ import {
VoiceOperationsContext,
VoiceStatus,
} from "../../../context/Voice";
import {
useForceUpdate,
useSelf,
useUsers,
} from "../../../context/revoltjs/hooks";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
......@@ -70,17 +67,20 @@ const VoiceBase = styled.div`
}
`;
export default function VoiceHeader({ id }: Props) {
export default observer(({ id }: Props) => {
const { status, participants, roomId } = useContext(VoiceContext);
if (roomId !== id) return null;
const { isProducing, startProducing, stopProducing, disconnect } =
useContext(VoiceOperationsContext);
const ctx = useForceUpdate();
const self = useSelf(ctx);
const client = useClient();
const self = client.users.get(client.user!._id);
//const ctx = useForceUpdate();
//const self = useSelf(ctx);
const keys = participants ? Array.from(participants.keys()) : undefined;
const users = keys ? useUsers(keys, ctx) : undefined;
const users = keys?.map((key) => client.users.get(key));
return (
<VoiceBase>
......@@ -135,7 +135,7 @@ export default function VoiceHeader({ id }: Props) {
</div>
</VoiceBase>
);
}
});
/**{voice.roomId === id && (
<div className={styles.rtc}>
......
import { Wrench } from "@styled-icons/boxicons-solid";
import { Channels } from "revolt.js/dist/api/objects";
import { useContext } from "preact/hooks";
......@@ -7,14 +6,13 @@ import PaintCounter from "../../lib/PaintCounter";
import { TextReact } from "../../lib/i18n";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { useData, useUserPermission } from "../../context/revoltjs/hooks";
import Header from "../../components/ui/Header";
export default function Developer() {
// const voice = useContext(VoiceContext);
const client = useContext(AppContext);
const userPermission = useUserPermission(client.user!._id);
const userPermission = client.user!.permission;
return (
<div>
......@@ -35,7 +33,6 @@ export default function Developer() {
fields={{ provider: <b>GAMING!</b> }}
/>
</div>
<DataTest />
<div style={{ padding: "16px" }}>
{/*<span>
<b>Voice Status:</b> {VoiceStatus[voice.status]}
......@@ -54,30 +51,3 @@ export default function Developer() {
</div>
);
}
function DataTest() {
const channel_id = (
useContext(AppContext)
.channels.toArray()
.find((x) => x.channel_type === "Group") as Channels.GroupChannel
)._id;
const data = useData(
(client) => {
return {
name: (client.channels.get(channel_id) as Channels.GroupChannel)
.name,
};
},
[{ key: "channels", id: channel_id }],
);
return (
<div style={{ padding: "16px" }}>
Channel name: {data.name}
<div style={{ width: "24px" }}>
<PaintCounter small />
</div>
</div>
);
}
import { X, Plus } from "@styled-icons/boxicons-regular";
import { PhoneCall, Envelope, UserX } from "@styled-icons/boxicons-solid";
import { User, Users } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Friend.module.scss";
import classNames from "classnames";
......@@ -12,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation";
import { VoiceOperationsContext } from "../../context/Voice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import {
AppContext,
OperationsContext,
} from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus";
......@@ -27,16 +26,15 @@ interface Props {
user: User;
}
export function Friend({ user }: Props) {
const client = useContext(AppContext);
export const Friend = observer(({ user }: Props) => {
const history = useHistory();
const { openScreen } = useIntermediate();
const { openDM } = useContext(OperationsContext);
const { connect } = useContext(VoiceOperationsContext);
const actions: Children[] = [];
let subtext: Children = null;
if (user.relationship === Users.Relationship.Friend) {
if (user.relationship === RelationshipStatus.Friend) {
subtext = <UserStatus user={user} />;
actions.push(
<>
......@@ -44,28 +42,41 @@ export function Friend({ user }: Props) {
type="circle"
className={classNames(styles.button, styles.success)}
onClick={(ev) =>
stopPropagation(ev, openDM(user._id).then(connect))
stopPropagation(
ev,
user
.openDM()
.then(connect)
.then((x) => history.push(`/channel/${x._id}`)),
)
}>
<PhoneCall size={20} />
</IconButton>
<IconButton
type="circle"
className={styles.button}
onClick={(ev) => stopPropagation(ev, openDM(user._id))}>
onClick={(ev) =>
stopPropagation(
ev,
user
.openDM()
.then((channel) =>
history.push(`/channel/${channel._id}`),
),
)
}>
<Envelope size={20} />
</IconButton>
</>,
);
}
if (user.relationship === Users.Relationship.Incoming) {
if (user.relationship === RelationshipStatus.Incoming) {
actions.push(
<IconButton
type="circle"
className={styles.button}
onClick={(ev) =>
stopPropagation(ev, client.users.addFriend(user.username))
}>
onClick={(ev) => stopPropagation(ev, user.addFriend())}>
<Plus size={24} />
</IconButton>,
);
......@@ -73,14 +84,14 @@ export function Friend({ user }: Props) {
subtext = <Text id="app.special.friends.incoming" />;
}
if (user.relationship === Users.Relationship.Outgoing) {
if (user.relationship === RelationshipStatus.Outgoing) {
subtext = <Text id="app.special.friends.outgoing" />;
}
if (
user.relationship === Users.Relationship.Friend ||
user.relationship === Users.Relationship.Outgoing ||
user.relationship === Users.Relationship.Incoming
user.relationship === RelationshipStatus.Friend ||
user.relationship === RelationshipStatus.Outgoing ||
user.relationship === RelationshipStatus.Incoming
) {
actions.push(
<IconButton
......@@ -93,13 +104,13 @@ export function Friend({ user }: Props) {
onClick={(ev) =>
stopPropagation(
ev,
user.relationship === Users.Relationship.Friend
user.relationship === RelationshipStatus.Friend
? openScreen({
id: "special_prompt",
type: "unfriend_user",
target: user,
})
: client.users.removeFriend(user._id),
: user.removeFriend(),
)
}>
<X size={24} />
......@@ -107,14 +118,12 @@ export function Friend({ user }: Props) {
);
}
if (user.relationship === Users.Relationship.Blocked) {
if (user.relationship === RelationshipStatus.Blocked) {
actions.push(
<IconButton
type="circle"
className={classNames(styles.button, styles.error)}
onClick={(ev) =>
stopPropagation(ev, client.users.unblockUser(user._id))
}>
onClick={(ev) => stopPropagation(ev, user.unblockUser())}>
<UserX size={24} />
</IconButton>,
);
......@@ -133,4 +142,4 @@ export function Friend({ user }: Props) {
<div className={styles.actions}>{actions}</div>
</div>
);
}
});
import {
ChevronDown,
ChevronRight,
ListPlus,
} from "@styled-icons/boxicons-regular";
import { ChevronRight } from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
import { User, Users } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { RelationshipStatus, Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Friend.module.scss";
import { Text } from "preact-i18n";
......@@ -13,65 +11,62 @@ import { TextReact } from "../../lib/i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { useUsers } from "../../context/revoltjs/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../components/common/CollapsibleSection";
import Tooltip from "../../components/common/Tooltip";
import UserIcon from "../../components/common/user/UserIcon";
import Details from "../../components/ui/Details";
import Header from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton";
import Overline from "../../components/ui/Overline";
import { Children } from "../../types/Preact";
import { Friend } from "./Friend";
export default function Friends() {
export default observer(() => {
const { openScreen } = useIntermediate();
const users = useUsers() as User[];
const client = useClient();
const users = [...client.users.values()];
users.sort((a, b) => a.username.localeCompare(b.username));
const friends = users.filter(
(x) => x.relationship === Users.Relationship.Friend,
(x) => x.relationship === RelationshipStatus.Friend,
);
const lists = [
[
"",
users.filter((x) => x.relationship === Users.Relationship.Incoming),
users.filter((x) => x.relationship === RelationshipStatus.Incoming),
],
[
"app.special.friends.sent",
users.filter((x) => x.relationship === Users.Relationship.Outgoing),
users.filter((x) => x.relationship === RelationshipStatus.Outgoing),
"outgoing",
],
[
"app.status.online",
friends.filter(
(x) =>
x.online && x.status?.presence !== Users.Presence.Invisible,
(x) => x.online && x.status?.presence !== Presence.Invisible,
),
"online",
],
[
"app.status.offline",
friends.filter(
(x) =>
!x.online ||
x.status?.presence === Users.Presence.Invisible,
(x) => !x.online || x.status?.presence === Presence.Invisible,
),
"offline",
],
[
"app.special.friends.blocked",
users.filter((x) => x.relationship === Users.Relationship.Blocked),
users.filter((x) => x.relationship === RelationshipStatus.Blocked),
"blocked",
],
] as [string, User[], string][];
const incoming = lists[0][1];
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>);
const userlist: Children[] = incoming.map((x) => (
<b key={x._id}>{x.username}</b>
));
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
......@@ -138,7 +133,7 @@ export default function Friends() {
onClick={() =>
openScreen({
id: "pending_requests",
users: incoming.map((x) => x._id),
users: incoming,
})
}>
<div className={styles.avatars}>
......@@ -198,6 +193,7 @@ export default function Friends() {
return (
<CollapsibleSection
key={section_id}
id={`friends_${section_id}`}
defaultValue={true}
sticky
......@@ -216,4 +212,4 @@ export default function Friends() {
</div>
</>
);
}
});
......@@ -17,7 +17,8 @@ export default function Home() {
<Text id="app.navigation.tabs.home" />
</Header>
<h3>
<Text id="app.special.modals.onboarding.welcome" />{" "}
<Text id="app.special.modals.onboarding.welcome" />
<br />
<img src={wideSVG} />
</h3>
<div className={styles.actions}>
......@@ -26,6 +27,14 @@ export default function Home() {
Join testers server
</Button>
</Link>
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<Button contrast gold>
Donate to Revolt
</Button>
</a>
<Link to="/settings/feedback">
<Button contrast>Give feedback</Button>
</Link>
......@@ -34,12 +43,6 @@ export default function Home() {
<Button contrast>Open settings</Button>
</Tooltip>
</Link>
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<Button contrast>Source code</Button>
</a>
</div>
</div>
);
......
import { ArrowBack } from "@styled-icons/boxicons-regular";
import { autorun } from "mobx";
import { useHistory, useParams } from "react-router-dom";
import { Invites } from "revolt.js/dist/api/objects";
import { RetrievedInvite } from "revolt-api/types/Invites";
import styles from "./Invite.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { defer } from "../../lib/defer";
import { TextReact } from "../../lib/i18n";
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import {
AppContext,
......@@ -26,7 +31,7 @@ export default function Invite() {
const { code } = useParams<{ code: string }>();
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState<Invites.RetrievedInvite | undefined>(
const [invite, setInvite] = useState<RetrievedInvite | undefined>(
undefined,
);
......@@ -40,7 +45,7 @@ export default function Invite() {
.then((data) => setInvite(data))
.catch((err) => setError(takeError(err)));
}
}, [status]);
}, [client, code, invite, status]);
if (typeof invite === "undefined") {
return (
......@@ -87,12 +92,20 @@ export default function Invite() {
<h1>{invite.server_name}</h1>
<h2>#{invite.channel_name}</h2>
<h3>
Invited by{" "}
<UserIcon
size={24}
attachment={invite.user_avatar}
/>{" "}
{invite.user_name}
<TextReact
id="app.special.invite.invited_by"
fields={{
user: (
<>
<UserIcon
size={24}
attachment={invite.user_avatar}
/>{" "}
{invite.user_name}
</>
),
}}
/>
</h3>
<Overline type="error" error={error} />
<Button
......@@ -113,24 +126,35 @@ export default function Invite() {
`/server/${invite.server_id}/channel/${invite.channel_id}`,
);
}
}
const result = await client.joinInvite(
code,
);
if (result.type === "Server") {
history.push(
`/server/${result.server._id}/channel/${result.channel._id}`,
);
const dispose = autorun(() => {
const server = client.servers.get(
invite.server_id,
);
defer(() => {
if (server) {
history.push(
`/server/${server._id}/channel/${invite.channel_id}`,
);
}
});
dispose();
});
}
await client.joinInvite(code);
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
}}>
{status === ClientStatus.READY
? "Login to Revolt"
: "Accept Invite"}
{status === ClientStatus.READY ? (
<Text id="app.special.invite.login" />
) : (
<Text id="app.special.invite.accept" />
)}
</Button>
</>
)}
......
import { UseFormMethods } from "react-hook-form";
import { Text, Localizer } from "preact-i18n";
import InputBox from "../../components/ui/InputBox";
......@@ -6,7 +8,7 @@ import Overline from "../../components/ui/Overline";
interface Props {
type: "email" | "username" | "password" | "invite" | "current_password";
showOverline?: boolean;
register: Function;
register: UseFormMethods["register"];
error?: string;
name?: string;
}
......@@ -27,9 +29,11 @@ export default function FormField({
)}
<Localizer>
<InputBox
// Styled uses React typing while we use Preact
// this leads to inconsistances where things need to be typed oddly
placeholder={(<Text id={`login.enter.${type}`} />) as any}
placeholder={
(
<Text id={`login.enter.${type}`} />
) as unknown as string
}
name={
type === "current_password" ? "password" : name ?? type
}
......@@ -40,6 +44,8 @@ export default function FormField({
? "password"
: type
}
// See https://github.com/mozilla/contain-facebook/issues/783
className="fbc-has-badge"
ref={register(
type === "password" || type === "current_password"
? {
......
......@@ -11,6 +11,7 @@ import { AppContext } from "../../context/revoltjs/RevoltClient";
import LocaleSelector from "../../components/common/LocaleSelector";
import { Titlebar } from "../../components/native/Titlebar";
import { APP_VERSION } from "../../version";
import background from "./background.jpg";
import { FormCreate } from "./forms/FormCreate";
......@@ -23,52 +24,57 @@ export default function Login() {
const client = useContext(AppContext);
return (
<div className={styles.login}>
<Helmet>
<meta name="theme-color" content={theme.background} />
</Helmet>
<div className={styles.content}>
<div className={styles.attribution}>
<span>
API:{" "}
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
&middot; App: <code>{APP_VERSION}</code>
</span>
<span>
<LocaleSelector />
</span>
</div>
<div className={styles.modal}>
<Switch>
<Route path="/login/create">
<FormCreate />
</Route>
<Route path="/login/resend">
<FormResend />
</Route>
<Route path="/login/reset/:token">
<FormReset />
</Route>
<Route path="/login/reset">
<FormSendReset />
</Route>
<Route path="/">
<FormLogin />
</Route>
</Switch>
</div>
<div className={styles.attribution}>
<span>
<Text id="general.image_by" /> &lrm;@lorenzoherrera
&rlm;· unsplash.com
</span>
<>
{window.isNative && !window.native.getConfig().frame && (
<Titlebar />
)}
<div className={styles.login}>
<Helmet>
<meta name="theme-color" content={theme.background} />
</Helmet>
<div className={styles.content}>
<div className={styles.attribution}>
<span>
API:{" "}
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
&middot; App: <code>{APP_VERSION}</code>
</span>
<span>
<LocaleSelector />
</span>
</div>
<div className={styles.modal}>
<Switch>
<Route path="/login/create">
<FormCreate />
</Route>
<Route path="/login/resend">
<FormResend />
</Route>
<Route path="/login/reset/:token">
<FormReset />
</Route>
<Route path="/login/reset">
<FormSendReset />
</Route>
<Route path="/">
<FormLogin />
</Route>
</Switch>
</div>
<div className={styles.attribution}>
<span>
<Text id="general.image_by" /> &lrm;@lorenzoherrera
&rlm;· unsplash.com
</span>
</div>
</div>
<div
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div>
<div
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div>
</>
);
}
......@@ -20,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) {
if (!client.configuration?.features.captcha.enabled) {
props.onSuccess();
}
}, []);
}, [client.configuration?.features.captcha.enabled, props]);
if (!client.configuration?.features.captcha.enabled)
return <Preloader type="spinner" />;
......
......@@ -63,7 +63,7 @@ export function Form({ page, callback }: Props) {
setGlobalError(undefined);
setLoading(true);
function onError(err: any) {
function onError(err: unknown) {
setLoading(false);
const error = takeError(err);
......
......@@ -19,7 +19,11 @@ export function FormLogin() {
let device_name;
if (browser) {
const { name, os } = browser;
device_name = `${name} on ${os}`;
if (window.isNative) {
device_name = `Revolt Desktop on ${os}`;
} else {
device_name = `${name} on ${os}`;
}
} else {
device_name = "Unknown Device";
}
......
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
import { Route, useHistory, useParams } from "react-router-dom";
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util";
import Category from "../../components/ui/Category";
......@@ -14,8 +14,11 @@ import Permissions from "./channel/Permissions";
export default function ChannelSettings() {
const { channel: cid } = useParams<{ channel: string }>();
const ctx = useForceUpdate();
const channel = useChannel(cid, ctx);
const client = useClient();
const history = useHistory();
const channel = client.channels.get(cid);
if (!channel) return null;
if (
channel.channel_type === "SavedMessages" ||
......@@ -23,13 +26,12 @@ export default function ChannelSettings() {
)
return null;
const history = useHistory();
function switchPage(to?: string) {
let base_url;
switch (channel?.channel_type) {
case "TextChannel":
case "VoiceChannel":
base_url = `/server/${channel.server}/channel/${cid}/settings`;
base_url = `/server/${channel.server_id}/channel/${cid}/settings`;
break;
default:
base_url = `/channel/${cid}/settings`;
......@@ -49,7 +51,7 @@ export default function ChannelSettings() {
category: (
<Category
variant="uniform"
text={getChannelName(ctx.client, channel, true)}
text={getChannelName(channel, true)}
/>
),
id: "overview",
......@@ -66,18 +68,20 @@ export default function ChannelSettings() {
),
},
]}
children={[
<Route path="/server/:server/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>,
<Route path="/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>,
children={
<Switch>
<Route path="/server/:server/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>
<Route path="/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>
<Route path="/">
<Overview channel={channel} />
</Route>,
]}
<Route>
<Overview channel={channel} />
</Route>
</Switch>
}
category="channel_pages"
switchPage={switchPage}
defaultPage="overview"
......
import { ArrowBack, X } from "@styled-icons/boxicons-regular";
import { Helmet } from "react-helmet";
import { Switch, useHistory, useParams } from "react-router-dom";
import { useHistory, useParams } from "react-router-dom";
import styles from "./Settings.module.scss";
import classNames from "classnames";
import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
......@@ -25,6 +26,7 @@ interface Props {
id: string;
icon: Children;
title: Children;
hidden?: boolean;
hideTitle?: boolean;
}[];
custom?: Children;
......@@ -48,13 +50,18 @@ export function GenericSettings({
const theme = useContext(ThemeContext);
const { page } = useParams<{ page: string }>();
function exitSettings() {
if (history.length > 0) {
history.goBack();
const [closing, setClosing] = useState(false);
const exitSettings = useCallback(() => {
if (history.length > 1) {
setClosing(true);
setTimeout(() => {
history.goBack();
}, 100);
} else {
history.push("/");
}
}
}, [history]);
useEffect(() => {
function keyDown(e: KeyboardEvent) {
......@@ -65,10 +72,15 @@ export function GenericSettings({
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, []);
}, [exitSettings]);
return (
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
<div
className={classNames(styles.settings, {
[styles.closing]: closing,
[styles.native]: window.isNative,
})}
data-mobile={isTouchscreenDevice}>
<Helmet>
<meta
name="theme-color"
......@@ -110,52 +122,64 @@ export function GenericSettings({
)}
{(!isTouchscreenDevice || typeof page === "undefined") && (
<div className={styles.sidebar}>
<div className={styles.container}>
{pages.map((entry, i) => (
<>
{entry.category && (
<Category
variant="uniform"
text={entry.category}
/>
)}
<ButtonItem
active={
page === entry.id ||
(i === 0 &&
!isTouchscreenDevice &&
typeof page === "undefined")
}
onClick={() => switchPage(entry.id)}
compact>
{entry.icon} {entry.title}
</ButtonItem>
{entry.divider && <LineDivider />}
</>
))}
{custom}
<div className={styles.scrollbox}>
<div className={styles.container}>
{pages.map((entry, i) =>
entry.hidden ? undefined : (
<>
{entry.category && (
<Category
variant="uniform"
text={entry.category}
/>
)}
<ButtonItem
active={
page === entry.id ||
(i === 0 &&
!isTouchscreenDevice &&
typeof page === "undefined")
}
onClick={() => switchPage(entry.id)}
compact>
{entry.icon} {entry.title}
</ButtonItem>
{entry.divider && <LineDivider />}
</>
),
)}
{custom}
</div>
</div>
</div>
)}
{(!isTouchscreenDevice || typeof page === "string") && (
<div className={styles.content}>
{!isTouchscreenDevice &&
!pages.find((x) => x.id === page && x.hideTitle) && (
<h1>
<Text
id={`app.settings.${category}.${
page ?? defaultPage
}.title`}
/>
</h1>
<div className={styles.scrollbox}>
<div className={styles.contentcontainer}>
{!isTouchscreenDevice &&
!pages.find(
(x) => x.id === page && x.hideTitle,
) && (
<h1>
<Text
id={`app.settings.${category}.${
page ?? defaultPage
}.title`}
/>
</h1>
)}
{children}
</div>
{!isTouchscreenDevice && (
<div className={styles.action}>
<div
onClick={exitSettings}
className={styles.closeButton}>
<X size={28} />
</div>
</div>
)}
<Switch>{children}</Switch>
</div>
)}
{!isTouchscreenDevice && (
<div className={styles.action}>
<div onClick={exitSettings} className={styles.closeButton}>
<X size={28} />
</div>
</div>
)}
......
import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
import { Route, useHistory, useParams } from "react-router-dom";
import { observer } from "mobx-react-lite";
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n";
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import { useServer } from "../../context/revoltjs/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import Category from "../../components/ui/Category";
......@@ -17,9 +18,10 @@ import { Members } from "./server/Members";
import { Overview } from "./server/Overview";
import { Roles } from "./server/Roles";
export default function ServerSettings() {
export default observer(() => {
const { server: sid } = useParams<{ server: string }>();
const server = useServer(sid);
const client = useClient();
const server = client.servers.get(sid);
if (!server) return null;
const history = useHistory();
......@@ -35,7 +37,7 @@ export default function ServerSettings() {
<GenericSettings
pages={[
{
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
category: <Category variant="uniform" text={server.name} />,
id: "overview",
icon: <ListUl size={20} />,
title: (
......@@ -75,38 +77,40 @@ export default function ServerSettings() {
hideTitle: true,
},
]}
children={[
<Route path="/server/:server/settings/categories">
<Categories server={server} />
</Route>,
<Route path="/server/:server/settings/members">
<RequiresOnline>
<Members server={server} />
</RequiresOnline>
</Route>,
<Route path="/server/:server/settings/invites">
<RequiresOnline>
<Invites server={server} />
</RequiresOnline>
</Route>,
<Route path="/server/:server/settings/bans">
<RequiresOnline>
<Bans server={server} />
</RequiresOnline>
</Route>,
<Route path="/server/:server/settings/roles">
<RequiresOnline>
<Roles server={server} />
</RequiresOnline>
</Route>,
<Route path="/">
<Overview server={server} />
</Route>,
]}
children={
<Switch>
<Route path="/server/:server/settings/categories">
<Categories server={server} />
</Route>
<Route path="/server/:server/settings/members">
<RequiresOnline>
<Members server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/invites">
<RequiresOnline>
<Invites server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/bans">
<RequiresOnline>
<Bans server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/roles">
<RequiresOnline>
<Roles server={server} />
</RequiresOnline>
</Route>
<Route>
<Overview server={server} />
</Route>
</Switch>
}
category="server_pages"
switchPage={switchPage}
defaultPage="overview"
showExitButton
/>
);
}
});
/* Settings animations */
@keyframes open {
0% {
transform: scale(1.2);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
......@@ -30,6 +33,7 @@
}
}
/* Settings CSS */
.settings[data-mobile="true"] {
flex-direction: column;
background: var(--primary-header);
......@@ -39,25 +43,41 @@
background: var(--primary-background);
}
.scrollbox {
&::-webkit-scrollbar-thumb {
border-top: none;
}
}
/* Sidebar */
.sidebar {
justify-content: flex-start;
overflow-y: auto;
.container {
padding: 20px 8px;
padding: 20px 8px calc(var(--bottom-navigation-height) + 30px);
min-width: 218px;
}
> div {
.scrollbox {
width: 100%;
}
.version {
place-items: center;
}
}
/* Content */
.content {
padding: 10px 12px var(--bottom-navigation-height);
padding: 0;
.scrollbox {
overflow: auto;
}
.contentcontainer {
max-width: unset !important;
padding: 16px 12px var(--bottom-navigation-height) !important;
}
}
}
......@@ -69,6 +89,10 @@
height: 100%;
position: fixed;
animation: open 0.18s ease-out, opacity 0.18s;
&.closing {
animation: close 0.18s ease-in;
}
}
.settings {
......@@ -76,21 +100,40 @@
display: flex;
user-select: none;
flex-direction: row;
justify-content: center;
background: var(--primary-background);
.scrollbox {
overflow-y: scroll;
visibility: hidden;
transition: visibility 0.1s;
}
.container,
.contentcontainer,
.scrollbox:hover,
.scrollbox:focus {
visibility: visible;
}
// All children receive custom scrollbar.
> * > ::-webkit-scrollbar-thumb {
width: 4px;
background-clip: content-box;
border-top: 80px solid transparent;
}
.sidebar {
flex: 2;
flex: 1 0 218px;
display: flex;
flex-shrink: 0;
overflow-y: scroll;
justify-content: flex-end;
background: var(--secondary-background);
.container {
width: 218px;
padding: 60px 8px;
height: fit-content;
min-width: 218px;
padding: 80px 8px;
display: flex;
gap: 2px;
flex-direction: column;
}
.divider {
......@@ -100,20 +143,17 @@
.donate {
color: goldenrod !important;
}
.logOut {
color: var(--error) !important;
}
.version {
margin: 1rem 12px 0;
font-size: 10px;
font-size: 0.625rem;
color: var(--secondary-foreground);
font-family: var(--monospace-font), monospace;
user-select: text;
display: grid;
//place-items: center;
> div {
gap: 2px;
......@@ -121,49 +161,63 @@
flex-direction: column;
}
.revision a:hover {
a:hover {
text-decoration: underline;
}
}
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.content {
flex: 3;
max-width: 740px;
padding: 60px 2em;
overflow-y: scroll;
overflow-x: hidden;
flex: 1 1 800px;
display: flex;
overflow-y: auto;
.scrollbox {
display: flex;
flex-grow: 1;
}
.contentcontainer {
display: flex;
gap: 13px;
height: fit-content;
max-width: 740px;
padding: 80px 32px;
width: 100%;
flex-direction: column;
}
details {
margin: 14px 0;
}
h1 {
margin-top: 0;
line-height: 1em;
font-size: 1.2em;
margin: 0;
line-height: 1rem;
font-size: 1.2rem;
font-weight: 600;
}
h3 {
font-size: 13px;
font-size: 0.8125rem;
text-transform: uppercase;
color: var(--secondary-foreground);
&:first-child {
margin-top: 0;
}
}
h4 {
margin: 4px 2px;
font-size: 13px;
font-size: 0.8125rem;
color: var(--tertiary-foreground);
text-transform: uppercase;
}
h5 {
margin-top: 0;
font-size: 12px;
font-size: 0.75rem;
font-weight: 400;
}
......@@ -171,29 +225,26 @@
border-top: 1px solid;
margin: 0;
padding-top: 5px;
font-size: 14px;
font-size: 0.875rem;
color: var(--secondary-foreground);
}
}
.action {
flex: 1;
flex-shrink: 0;
padding: 60px 8px;
color: var(--tertiary-background);
flex-grow: 1;
padding: 80px 8px;
visibility: visible;
position: sticky;
top: 0;
&:after {
content: "ESC";
margin-top: 4px;
display: flex;
text-align: center;
align-content: center;
justify-content: center;
position: relative;
color: var(--foreground);
width: 40px;
opacity: 0.5;
font-size: 0.75em;
font-size: 0.75rem;
}
.closeButton {
......@@ -209,28 +260,19 @@
svg {
color: var(--secondary-foreground);
}
&:hover {
background: var(--secondary-header);
}
&:active {
transform: translateY(2px);
}
}
> div {
display: inline;
}
}
section {
margin-bottom: 1em;
}
}
.loader {
> div {
margin: auto;
@media (pointer: coarse) {
.scrollbox {
visibility: visible !important;
overflow-y: auto;
}
}
......@@ -3,6 +3,7 @@ import {
Sync as SyncIcon,
Globe,
LogOut,
Desktop,
} from "@styled-icons/boxicons-regular";
import {
Bell,
......@@ -14,7 +15,7 @@ import {
User,
Megaphone,
} from "@styled-icons/boxicons-solid";
import { Route, useHistory } from "react-router-dom";
import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js";
import styles from "./Settings.module.scss";
......@@ -38,6 +39,7 @@ import { Appearance } from "./panes/Appearance";
import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages";
import { Native } from "./panes/Native";
import { Notifications } from "./panes/Notifications";
import { Profile } from "./panes/Profile";
import { Sessions } from "./panes/Sessions";
......@@ -100,6 +102,12 @@ export default function Settings() {
icon: <SyncIcon size={20} />,
title: <Text id="app.settings.pages.sync.title" />,
},
{
id: "native",
hidden: !window.isNative,
icon: <Desktop size={20} />,
title: <Text id="app.settings.pages.native.title" />,
},
{
divider: true,
id: "experiments",
......@@ -112,69 +120,74 @@ export default function Settings() {
title: <Text id="app.settings.pages.feedback.title" />,
},
]}
children={[
<Route path="/settings/profile">
<Profile />
</Route>,
<Route path="/settings/sessions">
<RequiresOnline>
<Sessions />
</RequiresOnline>
</Route>,
<Route path="/settings/appearance">
<Appearance />
</Route>,
<Route path="/settings/notifications">
<Notifications />
</Route>,
<Route path="/settings/language">
<Languages />
</Route>,
<Route path="/settings/sync">
<Sync />
</Route>,
<Route path="/settings/experiments">
<ExperimentsPage />
</Route>,
<Route path="/settings/feedback">
<Feedback />
</Route>,
<Route path="/">
<Account />
</Route>,
]}
children={
<Switch>
<Route path="/settings/profile">
<Profile />
</Route>
<Route path="/settings/sessions">
<RequiresOnline>
<Sessions />
</RequiresOnline>
</Route>
<Route path="/settings/appearance">
<Appearance />
</Route>
<Route path="/settings/notifications">
<Notifications />
</Route>
<Route path="/settings/language">
<Languages />
</Route>
<Route path="/settings/sync">
<Sync />
</Route>
<Route path="/settings/native">
<Native />
</Route>
<Route path="/settings/experiments">
<ExperimentsPage />
</Route>
<Route path="/settings/feedback">
<Feedback />
</Route>
<Route path="/">
<Account />
</Route>
</Switch>
}
defaultPage="account"
switchPage={switchPage}
category="pages"
custom={[
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<ButtonItem compact>
<Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
custom={
<>
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<ButtonItem compact>
<Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
</ButtonItem>
</a>
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<ButtonItem className={styles.donate} compact>
<Coffee size={20} />
<Text id="app.settings.pages.donate.title" />
</ButtonItem>
</a>
<LineDivider />
<ButtonItem
onClick={() => operations.logout()}
className={styles.logOut}
compact>
<LogOut size={20} />
<Text id="app.settings.pages.logOut" />
</ButtonItem>
</a>,
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<ButtonItem className={styles.donate} compact>
<Coffee size={20} />
<Text id="app.settings.pages.donate.title" />
</ButtonItem>
</a>,
<LineDivider />,
<ButtonItem
onClick={() => operations.logout()}
className={styles.logOut}
compact>
<LogOut size={20} />
<Text id="app.settings.pages.logOut" />
</ButtonItem>,
<div className={styles.version}>
<div>
<div className={styles.version}>
<span className={styles.revision}>
<a
href={`${REPO_URL}/${GIT_REVISION}`}
......@@ -198,13 +211,16 @@ export default function Settings() {
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
{APP_VERSION}
</span>
{window.isNative && (
<span>Native: {window.nativeVersion}</span>
)}
<span>
API: {client.configuration?.revolt ?? "N/A"}
</span>
<span>revolt.js: {LIBRARY_VERSION}</span>
</div>
</div>,
]}
</>
}
/>
);
}
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="468pt" height="617pt" viewBox="0 0 468 617"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.5, written by Peter Selinger 2001-2004
</metadata>
<g transform="translate(0,617) scale(0.709091,-0.709091)"
fill="#000099" stroke="none">
<path fill="#000099" stroke="none" d="M302 838 c-14 -14 -16 -126 -3 -147 5
-8 16 -11 25 -8 12 5 16 21 16 71 0 89 -10 112 -38 84z"/>
<path fill="#000099" stroke="none" d="M521 775 c-27 -57 -32 -108 -10 -113
18 -3 84 122 75 144 -11 30 -44 15 -65 -31z"/>
<path fill="#000099" stroke="none" d="M34 797 c-8 -22 59 -158 76 -154 38 7
-11 167 -51 167 -11 0 -22 -6 -25 -13z"/>
<path fill="#000099" stroke="none" d="M254 590 c-50 -7 -128 -52 -175 -100
-98 -100 -65 -346 57 -423 63 -40 107 -50 200 -44 125 7 212 62 275 172 53 92
32 220 -51 317 -62 71 -170 99 -306 78z"/>
<path fill="#ffff63" stroke="none" d="M443 539 c47 -13 112 -70 138 -120 24
-48 26 -147 3 -190 -22 -43 -82 -108 -117 -125 -137 -71 -277 -55 -351 41 -39
52 -51 92 -51 175 1 77 19 113 82 161 80 63 198 86 296 58z"/>
<path fill="#000099" stroke="none" d="M462 367 c-5 -7 -15 -28 -21 -48 -21
-67 -100 -120 -144 -98 -30 15 -65 56 -88 102 -21 40 -51 48 -57 14 -5 -26 53
-111 96 -141 89 -62 204 -7 252 119 15 40 -15 81 -38 52z"/>
</g>
</svg>
\ No newline at end of file
import { Channels } from "revolt.js/dist/api/objects";
import styled, { css } from "styled-components";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
interface Props {
channel:
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
channel: Channel;
}
const Row = styled.div`
......@@ -32,13 +29,11 @@ const Row = styled.div`
}
`;
export default function Overview({ channel }: Props) {
const client = useContext(AppContext);
const [name, setName] = useState(channel.name);
export default observer(({ channel }: Props) => {
const [name, setName] = useState(channel.name ?? undefined);
const [description, setDescription] = useState(channel.description ?? "");
useEffect(() => setName(channel.name), [channel.name]);
useEffect(() => setName(channel.name ?? undefined), [channel.name]);
useEffect(
() => setDescription(channel.description ?? ""),
[channel.description],
......@@ -46,12 +41,12 @@ export default function Overview({ channel }: Props) {
const [changed, setChanged] = useState(false);
function save() {
const changes: any = {};
const changes: Record<string, string | undefined> = {};
if (name !== channel.name) changes.name = name;
if (description !== channel.description)
changes.description = description;
client.channels.edit(channel._id, changes);
channel.edit(changes);
setChanged(false);
}
......@@ -65,17 +60,12 @@ export default function Overview({ channel }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={(icon) =>
client.channels.edit(channel._id, { icon })
}
previewURL={client.channels.getIconURL(
channel._id,
onUpload={(icon) => channel.edit({ icon })}
previewURL={channel.generateIconURL(
{ max_side: 256 },
true,
)}
remove={() =>
client.channels.edit(channel._id, { remove: "Icon" })
}
remove={() => channel.edit({ remove: "Icon" })}
defaultPreview={
channel.channel_type === "Group"
? "/assets/group.png"
......@@ -127,4 +117,4 @@ export default function Overview({ channel }: Props) {
</p>
</div>
);
}
});