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 1652 additions and 175 deletions
import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
import { useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import {
IntermediateContext,
useIntermediate,
} from "../../../context/intermediate/Intermediate";
import AutoComplete, {
useAutoComplete,
} from "../../../components/common/AutoComplete";
const EditorBase = styled.div`
display: flex;
flex-direction: column;
textarea {
resize: none;
padding: 12px;
white-space: pre-wrap;
font-size: var(--text-size);
border-radius: var(--border-radius);
background: var(--secondary-header);
}
.caption {
padding: 2px;
font-size: 11px;
color: var(--tertiary-foreground);
a {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
`;
interface Props {
message: Message;
finish: () => void;
}
export default function MessageEditor({ message, finish }: Props) {
const [content, setContent] = useState((message.content as string) ?? "");
const { focusTaken } = useContext(IntermediateContext);
const { openScreen } = useIntermediate();
async function save() {
finish();
if (content.length === 0) {
openScreen({
id: "special_prompt",
type: "delete_message",
target: message,
});
} else if (content !== message.content) {
await message.edit({
content,
});
}
}
// ? Stop editing when pressing ESC.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
finish();
}
}
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken, finish]);
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete((v) => setContent(v ?? ""), {
users: { type: "all" },
});
return (
<EditorBase>
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
forceFocus
maxRows={3}
value={content}
maxLength={2000}
padding="var(--message-box-padding)"
onChange={(ev) => {
onChange(ev);
setContent(ev.currentTarget.value);
}}
onKeyDown={(e) => {
if (onKeyDown(e)) return;
if (
!e.shiftKey &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
e.preventDefault();
save();
}
}}
onKeyUp={onKeyUp}
onFocus={onFocus}
onBlur={onBlur}
/>
<span className="caption">
escape to <a onClick={finish}>cancel</a> &middot; enter to{" "}
<a onClick={save}>save</a>
</span>
</EditorBase>
);
}
/* eslint-disable react-hooks/rules-of-hooks */
import { X } from "@styled-icons/boxicons-regular";
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 { useEffect, useState } from "preact/hooks";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
import { RenderState } from "../../../lib/renderer/types";
import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import Message from "../../../components/common/messaging/Message";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
import DateDivider from "../../../components/ui/DateDivider";
import Preloader from "../../../components/ui/Preloader";
import { Children } from "../../../types/Preact";
import ConversationStart from "./ConversationStart";
import MessageEditor from "./MessageEditor";
interface Props {
id: string;
state: RenderState;
highlight?: string;
queue: QueuedMessage[];
}
const BlockedMessage = styled.div`
font-size: 0.8em;
margin-top: 6px;
padding: 4px 64px;
color: var(--tertiary-foreground);
&:hover {
background: var(--hover);
}
`;
function MessageRenderer({ id, state, queue, highlight }: Props) {
if (state.type !== "RENDER") return null;
const client = useClient();
const userId = client.user!._id;
const [editing, setEditing] = useState<string | undefined>(undefined);
const stopEditing = () => {
setEditing(undefined);
internalEmit("TextArea", "focus", "message");
};
useEffect(() => {
function editLast() {
if (state.type !== "RENDER") return;
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author_id === userId) {
setEditing(state.messages[i]._id);
internalEmit("MessageArea", "jump_to_bottom");
return;
}
}
}
const subs = [
internalSubscribe("MessageRenderer", "edit_last", editLast),
internalSubscribe("MessageRenderer", "edit_message", setEditing),
];
return () => subs.forEach((unsub) => unsub());
}, [state.messages, state.type, userId]);
const render: Children[] = [];
let previous: MessageI | undefined;
if (state.atTop) {
render.push(<ConversationStart id={id} />);
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>,
);
}
let head = true;
function compare(
current: string,
curAuthor: string,
previous: string,
prevAuthor: string,
) {
const atime = decodeTime(current),
adate = new Date(atime),
btime = decodeTime(previous),
bdate = new Date(btime);
if (
adate.getFullYear() !== bdate.getFullYear() ||
adate.getMonth() !== bdate.getMonth() ||
adate.getDate() !== bdate.getDate()
) {
render.push(<DateDivider date={adate} />);
head = true;
}
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
}
let blocked = 0;
function pushBlocked() {
render.push(
<BlockedMessage>
<X size={16} />{" "}
<Text
id="app.main.channel.misc.blocked_messages"
fields={{ count: blocked }}
/>
</BlockedMessage>,
);
blocked = 0;
}
for (const message of state.messages) {
if (previous) {
compare(
message._id,
message.author_id,
previous._id,
previous.author_id,
);
}
if (message.author_id === SYSTEM_USER_ID) {
render.push(
<SystemMessage
key={message._id}
message={message}
attachContext
highlight={highlight === message._id}
/>,
);
} else if (
message.author?.relationship === RelationshipStatus.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}
/>,
);
}
previous = message;
}
if (blocked > 0) pushBlocked();
const nonces = state.messages.map((x) => x.nonce);
if (state.atBottom) {
for (const msg of queue) {
if (msg.channel !== id) continue;
if (nonces.includes(msg.id)) continue;
if (previous) {
compare(msg.id, userId!, previous._id, previous.author_id);
previous = {
_id: msg.id,
author_id: userId!,
} as MessageI;
}
render.push(
<Message
message={
new MessageI(client, {
...msg.data,
replies: msg.data.replies.map((x) => x.id),
})
}
key={msg.id}
queued={msg}
head={head}
attachContext
/>,
);
}
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>,
);
}
return <>{render}</>;
}
export default memo(
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
return {
queue: state.queue,
};
}),
);
import { BarChart } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import {
VoiceContext,
VoiceOperationsContext,
VoiceStatus,
} from "../../../context/Voice";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
interface Props {
id: string;
}
const VoiceBase = styled.div`
padding: 20px;
background: var(--secondary-background);
.status {
flex: 1 0;
display: flex;
position: absolute;
align-items: center;
padding: 10px;
font-size: 14px;
font-weight: 600;
user-select: none;
color: var(--success);
border-radius: var(--border-radius);
background: var(--primary-background);
svg {
margin-inline-end: 4px;
cursor: help;
}
}
display: flex;
flex-direction: column;
.participants {
margin: 20px 0;
justify-content: center;
pointer-events: none;
user-select: none;
display: flex;
gap: 16px;
.disconnected {
opacity: 0.5;
}
}
.actions {
display: flex;
justify-content: center;
gap: 10px;
}
`;
export default observer(({ id }: Props) => {
const { status, participants, roomId } = useContext(VoiceContext);
if (roomId !== id) return null;
const { isProducing, startProducing, stopProducing, disconnect } =
useContext(VoiceOperationsContext);
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?.map((key) => client.users.get(key));
return (
<VoiceBase>
<div className="participants">
{users && users.length !== 0
? users.map((user, index) => {
const id = keys![index];
return (
<div key={id}>
<UserIcon
size={80}
target={user}
status={false}
voice={
participants!.get(id)?.audio
? undefined
: "muted"
}
/>
</div>
);
})
: self !== undefined && (
<div key={self._id} className="disconnected">
<UserIcon
size={80}
target={self}
status={false}
/>
</div>
)}
</div>
<div className="status">
<BarChart size={20} />
{status === VoiceStatus.CONNECTED && (
<Text id="app.main.channel.voice.connected" />
)}
</div>
<div className="actions">
<Button error onClick={disconnect}>
<Text id="app.main.channel.voice.leave" />
</Button>
{isProducing("audio") ? (
<Button onClick={() => stopProducing("audio")}>
<Text id="app.main.channel.voice.mute" />
</Button>
) : (
<Button onClick={() => startProducing("audio")}>
<Text id="app.main.channel.voice.unmute" />
</Button>
)}
</div>
</VoiceBase>
);
});
/**{voice.roomId === id && (
<div className={styles.rtc}>
<div className={styles.participants}>
{participants.length !== 0 ? participants.map((user, index) => {
const id = participantIds[index];
return (
<div key={id}>
<UserIcon
size={80}
user={user}
status={false}
voice={
voice.participants.get(id)
?.audio
? undefined
: "muted"
}
/>
</div>
);
}) : self !== undefined && (
<div key={self._id} className={styles.disconnected}>
<UserIcon
size={80}
user={self}
status={false}
/>
</div>
)}
</div>
<div className={styles.status}>
<BarChart size={20} />
{ voice.status === VoiceStatus.CONNECTED && <Text id="app.main.channel.voice.connected" /> }
</div>
<div className={styles.actions}>
<Button
style="error"
onClick={() =>
voice.operations.disconnect()
}
>
<Text id="app.main.channel.voice.leave" />
</Button>
{voice.operations.isProducing("audio") ? (
<Button
onClick={() =>
voice.operations.stopProducing(
"audio"
)
}
>
<Text id="app.main.channel.voice.mute" />
</Button>
) : (
<Button
onClick={() =>
voice.operations.startProducing(
"audio"
)
}
>
<Text id="app.main.channel.voice.unmute" />
</Button>
)}
</div>
</div>
)} */
import { Wrench } from "@styled-icons/boxicons-solid";
import { useContext } from "preact/hooks";
import PaintCounter from "../../lib/PaintCounter";
import { TextReact } from "../../lib/i18n";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import Header from "../../components/ui/Header";
export default function Developer() {
// const voice = useContext(VoiceContext);
const client = useContext(AppContext);
const userPermission = client.user!.permission;
return (
<div>
<Header placement="primary">
<Wrench size="24" />
Developer Tab
</Header>
<div style={{ padding: "16px" }}>
<PaintCounter always />
</div>
<div style={{ padding: "16px" }}>
<b>User ID:</b> {client.user!._id} <br />
<b>Permission against self:</b> {userPermission} <br />
</div>
<div style={{ padding: "16px" }}>
<TextReact
id="login.open_mail_provider"
fields={{ provider: <b>GAMING!</b> }}
/>
</div>
<div style={{ padding: "16px" }}>
{/*<span>
<b>Voice Status:</b> {VoiceStatus[voice.status]}
</span>
<br />
<span>
<b>Voice Room ID:</b> {voice.roomId || "undefined"}
</span>
<br />
<span>
<b>Voice Participants:</b> [
{Array.from(voice.participants.keys()).join(", ")}]
</span>
<br />*/}
</div>
</div>
);
}
.title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.actions {
display: flex;
align-items: center;
gap: 20px;
}
.list {
padding: 0 10px 10px 10px;
user-select: none;
overflow-y: scroll;
&[data-empty="true"] {
img {
height: 120px;
border-radius: var(--border-radius);
}
gap: 16px;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
&[data-mobile="true"] {
padding-bottom: var(--bottom-navigation-height);
}
}
.friend {
height: 60px;
display: flex;
padding: 0 10px;
cursor: pointer;
align-items: center;
border-radius: var(--border-radius);
&:hover {
background: var(--secondary-background);
:global(.button) {
background-color: var(--primary-background);
}
}
.name {
flex-grow: 1;
margin: 0 12px;
font-size: 16px;
font-weight: 600;
display: flex;
flex-direction: column;
text-overflow: ellipsis;
overflow: hidden;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.subtext {
font-size: 12px;
font-weight: 400;
color: var(--tertiary-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.actions {
display: flex;
gap: 12px;
.button {
width: 36px;
height: 36px;
&:hover.error {
background: var(--error);
}
&:hover.success {
background: var(--success);
}
}
}
}
.divider {
width: 1px;
height: 24px;
background: var(--primary-background);
}
.title {
flex-grow: 1;
}
.pending {
padding: 1em;
display: flex;
cursor: pointer;
margin-top: 1em;
align-items: center;
flex-direction: row;
border-radius: var(--border-radius);
background: var(--secondary-background);
svg {
flex-shrink: 0;
}
.avatars {
display: flex;
flex-shrink: 0;
margin-inline-end: 15px;
svg:not(:first-child) {
position: relative;
margin-left: -32px;
}
}
.details {
flex-grow: 1;
display: flex;
flex-direction: column;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
> div {
font-size: 16px;
font-weight: 800;
display: flex;
gap: 6px;
align-items: center;
min-width: 0;
.title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
span {
width: 1.5em;
height: 1.5em;
font-size: 12px;
color: white;
border-radius: 50%;
align-items: center;
display: inline-flex;
justify-content: center;
background: var(--error);
flex-shrink: 0;
}
}
.from {
.user {
font-weight: 600;
}
}
> span {
font-weight: 400;
font-size: 12px;
color: var(--tertiary-foreground);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
@media only screen and (max-width: 768px) {
.list {
padding: 0 8px 8px 8px;
}
.remove {
display: none;
}
}
import { X, Plus } from "@styled-icons/boxicons-regular";
import { PhoneCall, Envelope, UserX } from "@styled-icons/boxicons-solid";
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";
import { attachContextMenu } from "preact-context-menu";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import { stopPropagation } from "../../lib/stopPropagation";
import { VoiceOperationsContext } from "../../context/Voice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import UserIcon from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus";
import IconButton from "../../components/ui/IconButton";
import { Children } from "../../types/Preact";
interface Props {
user: User;
}
export const Friend = observer(({ user }: Props) => {
const history = useHistory();
const { openScreen } = useIntermediate();
const { connect } = useContext(VoiceOperationsContext);
const actions: Children[] = [];
let subtext: Children = null;
if (user.relationship === RelationshipStatus.Friend) {
subtext = <UserStatus user={user} />;
actions.push(
<>
<IconButton
type="circle"
className={classNames(styles.button, styles.success)}
onClick={(ev) =>
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,
user
.openDM()
.then((channel) =>
history.push(`/channel/${channel._id}`),
),
)
}>
<Envelope size={20} />
</IconButton>
</>,
);
}
if (user.relationship === RelationshipStatus.Incoming) {
actions.push(
<IconButton
type="circle"
className={styles.button}
onClick={(ev) => stopPropagation(ev, user.addFriend())}>
<Plus size={24} />
</IconButton>,
);
subtext = <Text id="app.special.friends.incoming" />;
}
if (user.relationship === RelationshipStatus.Outgoing) {
subtext = <Text id="app.special.friends.outgoing" />;
}
if (
user.relationship === RelationshipStatus.Friend ||
user.relationship === RelationshipStatus.Outgoing ||
user.relationship === RelationshipStatus.Incoming
) {
actions.push(
<IconButton
type="circle"
className={classNames(
styles.button,
styles.remove,
styles.error,
)}
onClick={(ev) =>
stopPropagation(
ev,
user.relationship === RelationshipStatus.Friend
? openScreen({
id: "special_prompt",
type: "unfriend_user",
target: user,
})
: user.removeFriend(),
)
}>
<X size={24} />
</IconButton>,
);
}
if (user.relationship === RelationshipStatus.Blocked) {
actions.push(
<IconButton
type="circle"
className={classNames(styles.button, styles.error)}
onClick={(ev) => stopPropagation(ev, user.unblockUser())}>
<UserX size={24} />
</IconButton>,
);
}
return (
<div
className={styles.friend}
onClick={() => openScreen({ id: "profile", user_id: user._id })}
onContextMenu={attachContextMenu("Menu", { user: user._id })}>
<UserIcon target={user} size={36} status />
<div className={styles.name}>
<span>{user.username}</span>
{subtext && <span className={styles.subtext}>{subtext}</span>}
</div>
<div className={styles.actions}>{actions}</div>
</div>
);
});
import { ChevronRight } from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
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";
import { TextReact } from "../../lib/i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
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 Header from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton";
import { Children } from "../../types/Preact";
import { Friend } from "./Friend";
export default observer(() => {
const { openScreen } = useIntermediate();
const client = useClient();
const users = [...client.users.values()];
users.sort((a, b) => a.username.localeCompare(b.username));
const friends = users.filter(
(x) => x.relationship === RelationshipStatus.Friend,
);
const lists = [
[
"",
users.filter((x) => x.relationship === RelationshipStatus.Incoming),
],
[
"app.special.friends.sent",
users.filter((x) => x.relationship === RelationshipStatus.Outgoing),
"outgoing",
],
[
"app.status.online",
friends.filter(
(x) => x.online && x.status?.presence !== Presence.Invisible,
),
"online",
],
[
"app.status.offline",
friends.filter(
(x) => !x.online || x.status?.presence === Presence.Invisible,
),
"offline",
],
[
"app.special.friends.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 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;
return (
<>
<Header placement="primary">
{!isTouchscreenDevice && <UserDetail size={24} />}
<div className={styles.title}>
<Text id="app.navigation.tabs.friends" />
</div>
<div className={styles.actions}>
{/*<Tooltip content={"Create Category"} placement="bottom">
<IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_group' })}>
<ListPlus size={28} />
</IconButton>
</Tooltip>
<div className={styles.divider} />*/}
<Tooltip content={"Create Group"} placement="bottom">
<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_group",
})
}>
<MessageAdd size={24} />
</IconButton>
</Tooltip>
<Tooltip content={"Add Friend"} placement="bottom">
<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "add_friend",
})
}>
<UserPlus size={27} />
</IconButton>
</Tooltip>
{/*
<div className={styles.divider} />
<Tooltip content={"Friend Activity"} placement="bottom">
<IconButton>
<TennisBall size={24} />
</IconButton>
</Tooltip>
*/}
</div>
</Header>
<div
className={styles.list}
data-empty={isEmpty}
data-mobile={isTouchscreenDevice}>
{isEmpty && (
<>
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
<Text id="app.special.friends.nobody" />
</>
)}
{incoming.length > 0 && (
<div
className={styles.pending}
onClick={() =>
openScreen({
id: "pending_requests",
users: incoming,
})
}>
<div className={styles.avatars}>
{incoming.map(
(x, i) =>
i < 3 && (
<UserIcon
target={x}
size={64}
mask={
i <
Math.min(incoming.length - 1, 2)
? "url(#overlap)"
: undefined
}
/>
),
)}
</div>
<div className={styles.details}>
<div>
<Text id="app.special.friends.pending" />{" "}
<span>{incoming.length}</span>
</div>
<span>
{incoming.length > 3 ? (
<TextReact
id="app.special.friends.from.several"
fields={{
userlist: userlist.slice(0, 6),
count: incoming.length - 3,
}}
/>
) : incoming.length > 1 ? (
<TextReact
id="app.special.friends.from.multiple"
fields={{
user: userlist.shift()!,
userlist: userlist.slice(1),
}}
/>
) : (
<TextReact
id="app.special.friends.from.single"
fields={{ user: userlist[0] }}
/>
)}
</span>
</div>
<ChevronRight size={28} />
</div>
)}
{lists.map(([i18n, list, section_id], index) => {
if (index === 0) return;
if (list.length === 0) return;
return (
<CollapsibleSection
key={section_id}
id={`friends_${section_id}`}
defaultValue={true}
sticky
large
summary={
<div class="title">
<Text id={i18n} />{list.length}
</div>
}>
{list.map((x) => (
<Friend key={x._id} user={x} />
))}
</CollapsibleSection>
);
})}
</div>
</>
);
});
.home {
user-select: none;
h3 {
margin: 1em 0;
font-size: 48px;
text-align: center;
img {
height: 36px;
}
}
.actions {
gap: 8px;
margin: auto;
display: flex;
width: fit-content;
align-items: center;
flex-direction: column;
}
}
[data-light="true"] .home svg {
filter: invert(100%);
}
import { useChannels, useForceUpdate, useServers, useUser } from "../../context/revoltjs/hooks"; import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
import ChannelIcon from "../../components/common/ChannelIcon"; import { Link } from "react-router-dom";
import ServerIcon from "../../components/common/ServerIcon";
import UserIcon from "../../components/common/UserIcon";
import PaintCounter from "../../lib/PaintCounter";
export function Nested() { import styles from "./Home.module.scss";
const ctx = useForceUpdate(); import { Text } from "preact-i18n";
let user = useUser('01EX2NCWQ0CHS3QJF0FEQS1GR4', ctx)!; import wideSVG from "../../assets/wide.svg";
let user2 = useUser('01EX40TVKYNV114H8Q8VWEGBWQ', ctx)!; import Tooltip from "../../components/common/Tooltip";
let user3 = useUser('01F5GV44HTXP3MTCD2VPV42DPE', ctx)!; import Button from "../../components/ui/Button";
import Header from "../../components/ui/Header";
let channels = useChannels(undefined, ctx);
let servers = useServers(undefined, ctx);
return (
<>
<h3>Nested component</h3>
<PaintCounter />
@{ user.username } is { user.online ? 'online' : 'offline' }<br/><br/>
<h3>UserIcon Tests</h3>
<UserIcon size={64} target={user} />
<UserIcon size={64} target={user} status />
<UserIcon size={64} target={user} voice='muted' />
<UserIcon size={64} attachment={user2.avatar} />
<UserIcon size={64} attachment={user3.avatar} />
<UserIcon size={64} attachment={user3.avatar} animate />
<h3>Channels</h3>
{ channels.map(channel =>
channel &&
channel.channel_type !== 'SavedMessages' &&
channel.channel_type !== 'DirectMessage' &&
<ChannelIcon size={48} target={channel} />
) }
<h3>Servers</h3>
{ servers.map(server =>
server &&
<ServerIcon size={48} target={server} />
) }
<br/><br/>
<p>{ 'test long paragraph'.repeat(2000) }</p>
</>
)
}
export default function Home() { export default function Home() {
return ( return (
<div style={{ overflowY: 'scroll', height: '100vh' }}> <div className={styles.home}>
<h1>HOME</h1> <Header placement="primary">
<PaintCounter /> <HomeIcon size={24} />
<Nested /> <Text id="app.navigation.tabs.home" />
</Header>
<h3>
<Text id="app.special.modals.onboarding.welcome" />
<br />
<img src={wideSVG} />
</h3>
<div className={styles.actions}>
<Link to="/invite/Testers">
<Button contrast error>
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>
<Link to="/settings">
<Tooltip content="You can also right-click the user icon in the top left, or left click it if you're already home.">
<Button contrast>Open settings</Button>
</Tooltip>
</Link>
</div>
</div> </div>
); );
} }
.invite {
height: 100%;
display: flex;
color: white;
user-select: none;
align-items: center;
flex-direction: column;
background-size: cover;
justify-content: center;
background-position: center;
* {
overflow: visible;
}
.icon {
width: 64px;
z-index: 100;
text-align: left;
position: relative;
> * {
top: -32px;
position: absolute;
}
}
.leave {
top: 8px;
left: 8px;
cursor: pointer;
position: fixed;
}
.details {
text-align: center;
align-self: center;
padding: 32px 16px 16px 16px;
background: rgba(0, 0, 0, 0.6);
border-radius: var(--border-radius);
h1 {
margin: 0;
font-weight: 500;
}
h2 {
margin: 4px;
opacity: 0.7;
font-size: 0.8em;
font-weight: 400;
}
h3 {
gap: 8px;
display: flex;
font-size: 1em;
font-weight: 400;
flex-direction: row;
justify-content: center;
}
button {
margin: auto;
display: block;
background: rgba(0, 0, 0, 0.8);
}
}
}
.preloader {
height: 100%;
display: grid;
place-items: center;
}
import { ArrowBack } from "@styled-icons/boxicons-regular";
import { autorun } from "mobx";
import { useHistory, useParams } from "react-router-dom";
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,
ClientStatus,
StatusContext,
} from "../../context/revoltjs/RevoltClient";
import { takeError } from "../../context/revoltjs/util";
import ServerIcon from "../../components/common/ServerIcon";
import UserIcon from "../../components/common/user/UserIcon";
import Button from "../../components/ui/Button";
import Overline from "../../components/ui/Overline";
import Preloader from "../../components/ui/Preloader";
export default function Invite() {
const history = useHistory();
const client = useContext(AppContext);
const status = useContext(StatusContext);
const { code } = useParams<{ code: string }>();
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState<RetrievedInvite | undefined>(
undefined,
);
useEffect(() => {
if (
typeof invite === "undefined" &&
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
) {
client
.fetchInvite(code)
.then((data) => setInvite(data))
.catch((err) => setError(takeError(err)));
}
}, [client, code, invite, status]);
if (typeof invite === "undefined") {
return (
<div className={styles.preloader}>
<RequiresOnline>
{error ? (
<Overline type="error" error={error} />
) : (
<Preloader type="spinner" />
)}
</RequiresOnline>
</div>
);
}
// ! FIXME: add i18n translations
return (
<div
className={styles.invite}
style={{
backgroundImage: invite.server_banner
? `url('${client.generateFileURL(invite.server_banner)}')`
: undefined,
}}>
<div className={styles.leave}>
<ArrowBack size={32} onClick={() => history.push("/")} />
</div>
{!processing && (
<div className={styles.icon}>
<ServerIcon
attachment={invite.server_icon}
server_name={invite.server_name}
size={64}
/>
</div>
)}
<div className={styles.details}>
{processing ? (
<Preloader type="ring" />
) : (
<>
<h1>{invite.server_name}</h1>
<h2>#{invite.channel_name}</h2>
<h3>
<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
contrast
onClick={async () => {
if (status === ClientStatus.READY) {
return history.push("/");
}
try {
setProcessing(true);
if (invite.type === "Server") {
if (
client.servers.get(invite.server_id)
) {
history.push(
`/server/${invite.server_id}/channel/${invite.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 ? (
<Text id="app.special.invite.login" />
) : (
<Text id="app.special.invite.accept" />
)}
</Button>
</>
)}
</div>
</div>
);
}
import Overline from '../../components/ui/Overline'; import { UseFormMethods } from "react-hook-form";
import InputBox from '../../components/ui/InputBox';
import { Text, Localizer } from 'preact-i18n'; import { Text, Localizer } from "preact-i18n";
import InputBox from "../../components/ui/InputBox";
import Overline from "../../components/ui/Overline";
interface Props { interface Props {
type: "email" | "username" | "password" | "invite" | "current_password"; type: "email" | "username" | "password" | "invite" | "current_password";
showOverline?: boolean; showOverline?: boolean;
register: Function; register: UseFormMethods["register"];
error?: string; error?: string;
name?: string; name?: string;
} }
...@@ -15,7 +18,7 @@ export default function FormField({ ...@@ -15,7 +18,7 @@ export default function FormField({
register, register,
showOverline, showOverline,
error, error,
name name,
}: Props) { }: Props) {
return ( return (
<> <>
...@@ -26,7 +29,11 @@ export default function FormField({ ...@@ -26,7 +29,11 @@ export default function FormField({
)} )}
<Localizer> <Localizer>
<InputBox <InputBox
placeholder={(<Text id={`login.enter.${type}`} />) as any} placeholder={
(
<Text id={`login.enter.${type}`} />
) as unknown as string
}
name={ name={
type === "current_password" ? "password" : name ?? type type === "current_password" ? "password" : name ?? type
} }
...@@ -37,6 +44,8 @@ export default function FormField({ ...@@ -37,6 +44,8 @@ export default function FormField({
? "password" ? "password"
: type : type
} }
// See https://github.com/mozilla/contain-facebook/issues/783
className="fbc-has-badge"
ref={register( ref={register(
type === "password" || type === "current_password" type === "password" || type === "current_password"
? { ? {
...@@ -47,19 +56,19 @@ export default function FormField({ ...@@ -47,19 +56,19 @@ export default function FormField({
? "TooShort" ? "TooShort"
: value.length > 1024 : value.length > 1024
? "TooLong" ? "TooLong"
: undefined : undefined,
} }
: type === "email" : type === "email"
? { ? {
required: "RequiredField", required: "RequiredField",
pattern: { pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "InvalidEmail" message: "InvalidEmail",
} },
} }
: type === "username" : type === "username"
? { required: "RequiredField" } ? { required: "RequiredField" }
: { required: "RequiredField" } : { required: "RequiredField" },
)} )}
/> />
</Localizer> </Localizer>
......
import { Text } from "preact-i18n";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Route, Switch } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js";
import styles from "./Login.module.scss"; import styles from "./Login.module.scss";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { APP_VERSION } from "../../version";
import { LIBRARY_VERSION } from "revolt.js";
import { Route, Switch } from "react-router-dom";
import { ThemeContext } from "../../context/Theme"; import { ThemeContext } from "../../context/Theme";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import background from "./background.jpg"; import LocaleSelector from "../../components/common/LocaleSelector";
import { FormLogin } from "./forms/FormLogin"; import { Titlebar } from "../../components/native/Titlebar";
import { APP_VERSION } from "../../version";
import background from "./background.jpg";
import { FormCreate } from "./forms/FormCreate"; import { FormCreate } from "./forms/FormCreate";
import { FormLogin } from "./forms/FormLogin";
import { FormResend } from "./forms/FormResend"; import { FormResend } from "./forms/FormResend";
import { FormReset, FormSendReset } from "./forms/FormReset"; import { FormReset, FormSendReset } from "./forms/FormReset";
export default function Login() { export default function Login() {
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
const { client } = useContext(AppContext); const client = useContext(AppContext);
return ( return (
<div className={styles.login}> <>
<Helmet> {window.isNative && !window.native.getConfig().frame && (
<meta name="theme-color" content={theme.background} /> <Titlebar />
</Helmet> )}
<div className={styles.content}> <div className={styles.login}>
<div className={styles.attribution}> <Helmet>
<span> <meta name="theme-color" content={theme.background} />
API:{" "} </Helmet>
<code>{client.configuration?.revolt ?? "???"}</code>{" "} <div className={styles.content}>
&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "} <div className={styles.attribution}>
&middot; App: <code>{APP_VERSION}</code> <span>
</span> API:{" "}
<span> <code>{client.configuration?.revolt ?? "???"}</code>{" "}
{/*<LocaleSelector />*/} &middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
</span> &middot; App: <code>{APP_VERSION}</code>
</div> </span>
<div className={styles.modal}> <span>
<Switch> <LocaleSelector />
<Route path="/login/create"> </span>
<FormCreate /> </div>
</Route> <div className={styles.modal}>
<Route path="/login/resend"> <Switch>
<FormResend /> <Route path="/login/create">
</Route> <FormCreate />
<Route path="/login/reset/:token"> </Route>
<FormReset /> <Route path="/login/resend">
</Route> <FormResend />
<Route path="/login/reset"> </Route>
<FormSendReset /> <Route path="/login/reset/:token">
</Route> <FormReset />
<Route path="/"> </Route>
<FormLogin /> <Route path="/login/reset">
</Route> <FormSendReset />
</Switch> </Route>
</div> <Route path="/">
<div className={styles.attribution}> <FormLogin />
<span> </Route>
<Text id="general.image_by" /> &lrm;@lorenzoherrera </Switch>
&rlm;· unsplash.com </div>
</span> <div className={styles.attribution}>
<span>
<Text id="general.image_by" /> &lrm;@lorenzoherrera
&rlm;· unsplash.com
</span>
</div>
</div> </div>
<div
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div> </div>
<div </>
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div>
); );
}; }
import { Text } from "preact-i18n";
import styles from "../Login.module.scss";
import HCaptcha from "@hcaptcha/react-hcaptcha"; import HCaptcha from "@hcaptcha/react-hcaptcha";
import styles from "../Login.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import Preloader from "../../../components/ui/Preloader";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Preloader from "../../../components/ui/Preloader";
export interface CaptchaProps { export interface CaptchaProps {
onSuccess: (token?: string) => void; onSuccess: (token?: string) => void;
onCancel: () => void; onCancel: () => void;
} }
export function CaptchaBlock(props: CaptchaProps) { export function CaptchaBlock(props: CaptchaProps) {
const { client } = useContext(AppContext); const client = useContext(AppContext);
useEffect(() => { useEffect(() => {
if (!client.configuration?.features.captcha.enabled) { if (!client.configuration?.features.captcha.enabled) {
props.onSuccess(); props.onSuccess();
} }
}, []); }, [client.configuration?.features.captcha.enabled, props]);
if (!client.configuration?.features.captcha.enabled) if (!client.configuration?.features.captcha.enabled)
return <Preloader />; return <Preloader type="spinner" />;
return ( return (
<div> <div>
<HCaptcha <HCaptcha
sitekey={client.configuration.features.captcha.key} sitekey={client.configuration.features.captcha.key}
onVerify={token => props.onSuccess(token)} onVerify={(token) => props.onSuccess(token)}
/> />
<div className={styles.footer}> <div className={styles.footer}>
<a onClick={props.onCancel}> <a onClick={props.onCancel}>
......
import { Legal } from "./Legal"; import { CheckCircle, Envelope } from "@styled-icons/boxicons-regular";
import { Text } from "preact-i18n"; import { useForm } from "react-hook-form";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styles from "../Login.module.scss"; import styles from "../Login.module.scss";
import { useForm } from "react-hook-form"; import { Text } from "preact-i18n";
import { MailProvider } from "./MailProvider";
import { useContext, useState } from "preact/hooks"; import { useContext, useState } from "preact/hooks";
import { CheckCircle, Mail } from "@styled-icons/feather";
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
import { takeError } from "../../../context/revoltjs/error";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { takeError } from "../../../context/revoltjs/util";
import FormField from "../FormField"; import wideSVG from "../../../assets/wide.svg";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import Overline from "../../../components/ui/Overline"; import Overline from "../../../components/ui/Overline";
import Preloader from "../../../components/ui/Preloader"; import Preloader from "../../../components/ui/Preloader";
import FormField from "../FormField";
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
import { Legal } from "./Legal";
import { MailProvider } from "./MailProvider";
interface Props { interface Props {
page: "create" | "login" | "send_reset" | "reset" | "resend"; page: "create" | "login" | "send_reset" | "reset" | "resend";
callback: (fields: { callback: (fields: {
...@@ -26,38 +30,40 @@ interface Props { ...@@ -26,38 +30,40 @@ interface Props {
} }
function getInviteCode() { function getInviteCode() {
if (typeof window === 'undefined') return ''; if (typeof window === "undefined") return "";
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code'); const code = urlParams.get("code");
return code ?? ''; return code ?? "";
}
interface FormInputs {
email: string;
password: string;
invite: string;
} }
export function Form({ page, callback }: Props) { export function Form({ page, callback }: Props) {
const { client } = useContext(AppContext); const client = useContext(AppContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | undefined>(undefined); const [success, setSuccess] = useState<string | undefined>(undefined);
const [error, setGlobalError] = useState<string | undefined>(undefined); const [error, setGlobalError] = useState<string | undefined>(undefined);
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined); const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
const { handleSubmit, register, errors, setError } = useForm({ const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
defaultValues: { defaultValues: {
email: '', email: "",
password: '', password: "",
invite: getInviteCode() invite: getInviteCode(),
} },
}); });
async function onSubmit(data: { async function onSubmit(data: FormInputs) {
email: string;
password: string;
invite: string;
}) {
setGlobalError(undefined); setGlobalError(undefined);
setLoading(true); setLoading(true);
function onError(err: any) { function onError(err: unknown) {
setLoading(false); setLoading(false);
const error = takeError(err); const error = takeError(err);
...@@ -79,7 +85,7 @@ export function Form({ page, callback }: Props) { ...@@ -79,7 +85,7 @@ export function Form({ page, callback }: Props) {
page !== "reset" page !== "reset"
) { ) {
setCaptcha({ setCaptcha({
onSuccess: async captcha => { onSuccess: async (captcha) => {
setCaptcha(undefined); setCaptcha(undefined);
try { try {
await callback({ ...data, captcha }); await callback({ ...data, captcha });
...@@ -91,7 +97,7 @@ export function Form({ page, callback }: Props) { ...@@ -91,7 +97,7 @@ export function Form({ page, callback }: Props) {
onCancel: () => { onCancel: () => {
setCaptcha(undefined); setCaptcha(undefined);
setLoading(false); setLoading(false);
} },
}); });
} else { } else {
await callback(data); await callback(data);
...@@ -107,7 +113,7 @@ export function Form({ page, callback }: Props) { ...@@ -107,7 +113,7 @@ export function Form({ page, callback }: Props) {
<div className={styles.success}> <div className={styles.success}>
{client.configuration?.features.email ? ( {client.configuration?.features.email ? (
<> <>
<Mail size={72} /> <Envelope size={72} />
<h2> <h2>
<Text id="login.check_mail" /> <Text id="login.check_mail" />
</h2> </h2>
...@@ -136,11 +142,18 @@ export function Form({ page, callback }: Props) { ...@@ -136,11 +142,18 @@ export function Form({ page, callback }: Props) {
} }
if (captcha) return <CaptchaBlock {...captcha} />; if (captcha) return <CaptchaBlock {...captcha} />;
if (loading) return <Preloader />; if (loading) return <Preloader type="spinner" />;
return ( return (
<div className={styles.form}> <div className={styles.form}>
<form onSubmit={handleSubmit(onSubmit) as any}> <img src={wideSVG} />
{/* Preact / React typing incompatabilities */}
<form
onSubmit={
handleSubmit(
onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement>
}>
{page !== "reset" && ( {page !== "reset" && (
<FormField <FormField
type="email" type="email"
......
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form"; import { Form } from "./Form";
export function FormCreate() { export function FormCreate() {
const { client } = useContext(AppContext); const client = useContext(AppContext);
return ( return (
<Form <Form
page="create" page="create"
callback={async data => { callback={async (data) => {
await client.register(import.meta.env.VITE_API_URL, data); await client.register(import.meta.env.VITE_API_URL, data);
}} }}
/> />
......
import { Form } from "./Form"; import { detect } from "detect-browser";
import { useContext } from "preact/hooks";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { deviceDetect } from "react-device-detect";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { useContext } from "preact/hooks";
import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormLogin() { export function FormLogin() {
const { operations } = useContext(AppContext); const { login } = useContext(OperationsContext);
const history = useHistory(); const history = useHistory();
return ( return (
<Form <Form
page="login" page="login"
callback={async data => { callback={async (data) => {
const browser = deviceDetect(); const browser = detect();
let device_name; let device_name;
if (browser) { if (browser) {
const { name, os } = 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 { } else {
device_name = "Unknown Device"; device_name = "Unknown Device";
} }
await operations.login({ ...data, device_name }); await login({ ...data, device_name });
history.push("/"); history.push("/");
}} }}
/> />
......
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form"; import { Form } from "./Form";
export function FormResend() { export function FormResend() {
const { client } = useContext(AppContext); const client = useContext(AppContext);
return ( return (
<Form <Form
page="resend" page="resend"
callback={async data => { callback={async (data) => {
await client.req("POST", "/auth/resend", data); await client.req("POST", "/auth/resend", data);
}} }}
/> />
......
import { Form } from "./Form";
import { useContext } from "preact/hooks";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormSendReset() { export function FormSendReset() {
const { client } = useContext(AppContext); const client = useContext(AppContext);
return ( return (
<Form <Form
page="send_reset" page="send_reset"
callback={async data => { callback={async (data) => {
await client.req("POST", "/auth/send_reset", data); await client.req("POST", "/auth/send_reset", data);
}} }}
/> />
...@@ -18,16 +21,16 @@ export function FormSendReset() { ...@@ -18,16 +21,16 @@ export function FormSendReset() {
export function FormReset() { export function FormReset() {
const { token } = useParams<{ token: string }>(); const { token } = useParams<{ token: string }>();
const { client } = useContext(AppContext); const client = useContext(AppContext);
const history = useHistory(); const history = useHistory();
return ( return (
<Form <Form
page="reset" page="reset"
callback={async data => { callback={async (data) => {
await client.req("POST", "/auth/reset" as any, { await client.req("POST", "/auth/reset", {
token, token,
...(data as any) ...data,
}); });
history.push("/login"); history.push("/login");
}} }}
......
...@@ -7,21 +7,21 @@ export function Legal() { ...@@ -7,21 +7,21 @@ export function Legal() {
<a <a
href="https://revolt.chat/about" href="https://revolt.chat/about"
target="_blank" target="_blank"
> rel="noreferrer">
<Text id="general.about" /> <Text id="general.about" />
</a> </a>
&middot; &middot;
<a <a
href="https://revolt.chat/terms" href="https://revolt.chat/terms"
target="_blank" target="_blank"
> rel="noreferrer">
<Text id="general.tos" /> <Text id="general.tos" />
</a> </a>
&middot; &middot;
<a <a
href="https://revolt.chat/privacy" href="https://revolt.chat/privacy"
target="_blank" target="_blank"
> rel="noreferrer">
<Text id="general.privacy" /> <Text id="general.privacy" />
</a> </a>
</span> </span>
......