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 632 additions and 405 deletions
...@@ -6,14 +6,13 @@ import PaintCounter from "../../lib/PaintCounter"; ...@@ -6,14 +6,13 @@ import PaintCounter from "../../lib/PaintCounter";
import { TextReact } from "../../lib/i18n"; import { TextReact } from "../../lib/i18n";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import { useUserPermission } from "../../context/revoltjs/hooks";
import Header from "../../components/ui/Header"; import Header from "../../components/ui/Header";
export default function Developer() { export default function Developer() {
// const voice = useContext(VoiceContext); // const voice = useContext(VoiceContext);
const client = useContext(AppContext); const client = useContext(AppContext);
const userPermission = useUserPermission(client.user!._id); const userPermission = client.user!.permission;
return ( return (
<div> <div>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
&[data-empty="true"] { &[data-empty="true"] {
img { img {
height: 120px; height: 120px;
border-radius: 8px; border-radius: var(--border-radius);
} }
gap: 16px; gap: 16px;
...@@ -35,12 +35,12 @@ ...@@ -35,12 +35,12 @@
} }
.friend { .friend {
padding: 0 10px;
height: 60px; height: 60px;
display: flex; display: flex;
border-radius: 5px; padding: 0 10px;
align-items: center;
cursor: pointer; cursor: pointer;
align-items: center;
border-radius: var(--border-radius);
&:hover { &:hover {
background: var(--secondary-background); background: var(--secondary-background);
...@@ -110,9 +110,9 @@ ...@@ -110,9 +110,9 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
margin-top: 1em; margin-top: 1em;
border-radius: 7px;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
border-radius: var(--border-radius);
background: var(--secondary-background); background: var(--secondary-background);
svg { svg {
...@@ -191,7 +191,7 @@ ...@@ -191,7 +191,7 @@
padding: 0 8px 8px 8px; padding: 0 8px 8px 8px;
} }
.call { .remove {
display: none; display: none;
} }
} }
import { X, Plus } from "@styled-icons/boxicons-regular"; import { X, Plus } from "@styled-icons/boxicons-regular";
import { PhoneCall, Envelope } from "@styled-icons/boxicons-solid"; 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 styles from "./Friend.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -12,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation"; ...@@ -12,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation";
import { VoiceOperationsContext } from "../../context/Voice"; import { VoiceOperationsContext } from "../../context/Voice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import {
AppContext,
OperationsContext,
} from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus"; import UserStatus from "../../components/common/user/UserStatus";
...@@ -27,49 +26,57 @@ interface Props { ...@@ -27,49 +26,57 @@ interface Props {
user: User; user: User;
} }
export function Friend({ user }: Props) { export const Friend = observer(({ user }: Props) => {
const client = useContext(AppContext); const history = useHistory();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const { openDM } = useContext(OperationsContext);
const { connect } = useContext(VoiceOperationsContext); const { connect } = useContext(VoiceOperationsContext);
const actions: Children[] = []; const actions: Children[] = [];
let subtext: Children = null; let subtext: Children = null;
if (user.relationship === Users.Relationship.Friend) { if (user.relationship === RelationshipStatus.Friend) {
subtext = <UserStatus user={user} />; subtext = <UserStatus user={user} />;
actions.push( actions.push(
<> <>
<IconButton <IconButton
type="circle" type="circle"
className={classNames( className={classNames(styles.button, styles.success)}
styles.button,
styles.call,
styles.success,
)}
onClick={(ev) => 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} /> <PhoneCall size={20} />
</IconButton> </IconButton>
<IconButton <IconButton
type="circle" type="circle"
className={styles.button} 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} /> <Envelope size={20} />
</IconButton> </IconButton>
</>, </>,
); );
} }
if (user.relationship === Users.Relationship.Incoming) { if (user.relationship === RelationshipStatus.Incoming) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={styles.button} className={styles.button}
onClick={(ev) => onClick={(ev) => stopPropagation(ev, user.addFriend())}>
stopPropagation(ev, client.users.addFriend(user.username))
}>
<Plus size={24} /> <Plus size={24} />
</IconButton>, </IconButton>,
); );
...@@ -77,29 +84,33 @@ export function Friend({ user }: Props) { ...@@ -77,29 +84,33 @@ export function Friend({ user }: Props) {
subtext = <Text id="app.special.friends.incoming" />; 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" />; subtext = <Text id="app.special.friends.outgoing" />;
} }
if ( if (
user.relationship === Users.Relationship.Friend || user.relationship === RelationshipStatus.Friend ||
user.relationship === Users.Relationship.Outgoing || user.relationship === RelationshipStatus.Outgoing ||
user.relationship === Users.Relationship.Incoming user.relationship === RelationshipStatus.Incoming
) { ) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={classNames(styles.button, styles.error)} className={classNames(
styles.button,
styles.remove,
styles.error,
)}
onClick={(ev) => onClick={(ev) =>
stopPropagation( stopPropagation(
ev, ev,
user.relationship === Users.Relationship.Friend user.relationship === RelationshipStatus.Friend
? openScreen({ ? openScreen({
id: "special_prompt", id: "special_prompt",
type: "unfriend_user", type: "unfriend_user",
target: user, target: user,
}) })
: client.users.removeFriend(user._id), : user.removeFriend(),
) )
}> }>
<X size={24} /> <X size={24} />
...@@ -107,15 +118,13 @@ export function Friend({ user }: Props) { ...@@ -107,15 +118,13 @@ export function Friend({ user }: Props) {
); );
} }
if (user.relationship === Users.Relationship.Blocked) { if (user.relationship === RelationshipStatus.Blocked) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={classNames(styles.button, styles.error)} className={classNames(styles.button, styles.error)}
onClick={(ev) => onClick={(ev) => stopPropagation(ev, user.unblockUser())}>
stopPropagation(ev, client.users.unblockUser(user._id)) <UserX size={24} />
}>
<X size={24} />
</IconButton>, </IconButton>,
); );
} }
...@@ -127,10 +136,10 @@ export function Friend({ user }: Props) { ...@@ -127,10 +136,10 @@ export function Friend({ user }: Props) {
onContextMenu={attachContextMenu("Menu", { user: user._id })}> onContextMenu={attachContextMenu("Menu", { user: user._id })}>
<UserIcon target={user} size={36} status /> <UserIcon target={user} size={36} status />
<div className={styles.name}> <div className={styles.name}>
<span>@{user.username}</span> <span>{user.username}</span>
{subtext && <span className={styles.subtext}>{subtext}</span>} {subtext && <span className={styles.subtext}>{subtext}</span>}
</div> </div>
<div className={styles.actions}>{actions}</div> <div className={styles.actions}>{actions}</div>
</div> </div>
); );
} });
import { import { ChevronRight } from "@styled-icons/boxicons-regular";
ChevronDown,
ChevronRight,
ListPlus,
} from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid"; 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 styles from "./Friend.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -13,65 +11,62 @@ import { TextReact } from "../../lib/i18n"; ...@@ -13,65 +11,62 @@ import { TextReact } from "../../lib/i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; 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 CollapsibleSection from "../../components/common/CollapsibleSection";
import Tooltip from "../../components/common/Tooltip"; import Tooltip from "../../components/common/Tooltip";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import Details from "../../components/ui/Details";
import Header from "../../components/ui/Header"; import Header from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton"; import IconButton from "../../components/ui/IconButton";
import Overline from "../../components/ui/Overline";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import { Friend } from "./Friend"; import { Friend } from "./Friend";
export default function Friends() { export default observer(() => {
const { openScreen } = useIntermediate(); 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)); users.sort((a, b) => a.username.localeCompare(b.username));
const friends = users.filter( const friends = users.filter(
(x) => x.relationship === Users.Relationship.Friend, (x) => x.relationship === RelationshipStatus.Friend,
); );
const lists = [ const lists = [
[ [
"", "",
users.filter((x) => x.relationship === Users.Relationship.Incoming), users.filter((x) => x.relationship === RelationshipStatus.Incoming),
], ],
[ [
"app.special.friends.sent", "app.special.friends.sent",
users.filter((x) => x.relationship === Users.Relationship.Outgoing), users.filter((x) => x.relationship === RelationshipStatus.Outgoing),
"outgoing", "outgoing",
], ],
[ [
"app.status.online", "app.status.online",
friends.filter( friends.filter(
(x) => (x) => x.online && x.status?.presence !== Presence.Invisible,
x.online && x.status?.presence !== Users.Presence.Invisible,
), ),
"online", "online",
], ],
[ [
"app.status.offline", "app.status.offline",
friends.filter( friends.filter(
(x) => (x) => !x.online || x.status?.presence === Presence.Invisible,
!x.online ||
x.status?.presence === Users.Presence.Invisible,
), ),
"offline", "offline",
], ],
[ [
"app.special.friends.blocked", "app.special.friends.blocked",
users.filter((x) => x.relationship === Users.Relationship.Blocked), users.filter((x) => x.relationship === RelationshipStatus.Blocked),
"blocked", "blocked",
], ],
] as [string, User[], string][]; ] as [string, User[], string][];
const incoming = lists[0][1]; 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, ", "); 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; const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
...@@ -121,7 +116,10 @@ export default function Friends() { ...@@ -121,7 +116,10 @@ export default function Friends() {
*/} */}
</div> </div>
</Header> </Header>
<div className={styles.list} data-empty={isEmpty} data-mobile={isTouchscreenDevice}> <div
className={styles.list}
data-empty={isEmpty}
data-mobile={isTouchscreenDevice}>
{isEmpty && ( {isEmpty && (
<> <>
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" /> <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
...@@ -135,7 +133,7 @@ export default function Friends() { ...@@ -135,7 +133,7 @@ export default function Friends() {
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "pending_requests", id: "pending_requests",
users: incoming.map((x) => x._id), users: incoming,
}) })
}> }>
<div className={styles.avatars}> <div className={styles.avatars}>
...@@ -195,6 +193,7 @@ export default function Friends() { ...@@ -195,6 +193,7 @@ export default function Friends() {
return ( return (
<CollapsibleSection <CollapsibleSection
key={section_id}
id={`friends_${section_id}`} id={`friends_${section_id}`}
defaultValue={true} defaultValue={true}
sticky sticky
...@@ -210,9 +209,7 @@ export default function Friends() { ...@@ -210,9 +209,7 @@ export default function Friends() {
</CollapsibleSection> </CollapsibleSection>
); );
})} })}
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><h1>test</h1>
</div> </div>
</> </>
); );
} });
...@@ -11,15 +11,13 @@ ...@@ -11,15 +11,13 @@
} }
} }
ul { .actions {
gap: 8px;
margin: auto; margin: auto;
display: block; display: flex;
font-size: 18px; width: fit-content;
text-align: center; align-items: center;
flex-direction: column;
li {
list-style: lower-greek;
}
} }
} }
......
...@@ -5,6 +5,8 @@ import styles from "./Home.module.scss"; ...@@ -5,6 +5,8 @@ import styles from "./Home.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import wideSVG from "../../assets/wide.svg"; import wideSVG from "../../assets/wide.svg";
import Tooltip from "../../components/common/Tooltip";
import Button from "../../components/ui/Button";
import Header from "../../components/ui/Header"; import Header from "../../components/ui/Header";
export default function Home() { export default function Home() {
...@@ -15,27 +17,33 @@ export default function Home() { ...@@ -15,27 +17,33 @@ export default function Home() {
<Text id="app.navigation.tabs.home" /> <Text id="app.navigation.tabs.home" />
</Header> </Header>
<h3> <h3>
<Text id="app.special.modals.onboarding.welcome" />{" "} <Text id="app.special.modals.onboarding.welcome" />
<br />
<img src={wideSVG} /> <img src={wideSVG} />
</h3> </h3>
<ul> <div className={styles.actions}>
<li> <Link to="/invite/Testers">
Go to your <Link to="/friends">friends list</Link>. <Button contrast error>
</li> Join testers server
<li> </Button>
Give <Link to="/settings/feedback">feedback</Link>. </Link>
</li> <a
<li> href="https://insrt.uk/donate"
Join <Link to="/invite/Testers">testers server</Link>. target="_blank"
</li> rel="noreferrer">
<li> <Button contrast gold>
View{" "} Donate to Revolt
<a href="https://gitlab.insrt.uk/revolt" target="_blank"> </Button>
source code </a>
</a> <Link to="/settings/feedback">
. <Button contrast>Give feedback</Button>
</li> </Link>
</ul> <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>
); );
} }
...@@ -34,10 +34,10 @@ ...@@ -34,10 +34,10 @@
.details { .details {
text-align: center; text-align: center;
border-radius: 3px;
align-self: center; align-self: center;
padding: 32px 16px 16px 16px; padding: 32px 16px 16px 16px;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
border-radius: var(--border-radius);
h1 { h1 {
margin: 0; margin: 0;
......
import { ArrowBack } from "@styled-icons/boxicons-regular"; import { ArrowBack } from "@styled-icons/boxicons-regular";
import { autorun } from "mobx";
import { useHistory, useParams } from "react-router-dom"; 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 styles from "./Invite.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { defer } from "../../lib/defer";
import { TextReact } from "../../lib/i18n";
import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import { import {
AppContext, AppContext,
...@@ -26,7 +31,7 @@ export default function Invite() { ...@@ -26,7 +31,7 @@ export default function Invite() {
const { code } = useParams<{ code: string }>(); const { code } = useParams<{ code: string }>();
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState<Invites.RetrievedInvite | undefined>( const [invite, setInvite] = useState<RetrievedInvite | undefined>(
undefined, undefined,
); );
...@@ -40,7 +45,7 @@ export default function Invite() { ...@@ -40,7 +45,7 @@ export default function Invite() {
.then((data) => setInvite(data)) .then((data) => setInvite(data))
.catch((err) => setError(takeError(err))); .catch((err) => setError(takeError(err)));
} }
}, [status]); }, [client, code, invite, status]);
if (typeof invite === "undefined") { if (typeof invite === "undefined") {
return ( return (
...@@ -87,12 +92,20 @@ export default function Invite() { ...@@ -87,12 +92,20 @@ export default function Invite() {
<h1>{invite.server_name}</h1> <h1>{invite.server_name}</h1>
<h2>#{invite.channel_name}</h2> <h2>#{invite.channel_name}</h2>
<h3> <h3>
Invited by{" "} <TextReact
<UserIcon id="app.special.invite.invited_by"
size={24} fields={{
attachment={invite.user_avatar} user: (
/>{" "} <>
{invite.user_name} <UserIcon
size={24}
attachment={invite.user_avatar}
/>{" "}
{invite.user_name}
</>
),
}}
/>
</h3> </h3>
<Overline type="error" error={error} /> <Overline type="error" error={error} />
<Button <Button
...@@ -105,20 +118,43 @@ export default function Invite() { ...@@ -105,20 +118,43 @@ export default function Invite() {
try { try {
setProcessing(true); setProcessing(true);
let result = await client.joinInvite(code); if (invite.type === "Server") {
if (result.type === "Server") { if (
history.push( client.servers.get(invite.server_id)
`/server/${result.server._id}/channel/${result.channel._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) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
setProcessing(false); setProcessing(false);
} }
}}> }}>
{status === ClientStatus.READY {status === ClientStatus.READY ? (
? "Login to REVOLT" <Text id="app.special.invite.login" />
: "Accept Invite"} ) : (
<Text id="app.special.invite.accept" />
)}
</Button> </Button>
</> </>
)} )}
......
import { UseFormMethods } from "react-hook-form";
import { Text, Localizer } from "preact-i18n"; import { Text, Localizer } from "preact-i18n";
import InputBox from "../../components/ui/InputBox"; import InputBox from "../../components/ui/InputBox";
...@@ -6,7 +8,7 @@ import Overline from "../../components/ui/Overline"; ...@@ -6,7 +8,7 @@ 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;
} }
...@@ -27,9 +29,11 @@ export default function FormField({ ...@@ -27,9 +29,11 @@ export default function FormField({
)} )}
<Localizer> <Localizer>
<InputBox <InputBox
// Styled uses React typing while we use Preact placeholder={
// this leads to inconsistances where things need to be typed oddly (
placeholder={(<Text id={`login.enter.${type}`} />) as any} <Text id={`login.enter.${type}`} />
) as unknown as string
}
name={ name={
type === "current_password" ? "password" : name ?? type type === "current_password" ? "password" : name ?? type
} }
...@@ -40,6 +44,8 @@ export default function FormField({ ...@@ -40,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"
? { ? {
......
...@@ -11,6 +11,7 @@ import { AppContext } from "../../context/revoltjs/RevoltClient"; ...@@ -11,6 +11,7 @@ import { AppContext } from "../../context/revoltjs/RevoltClient";
import LocaleSelector from "../../components/common/LocaleSelector"; import LocaleSelector from "../../components/common/LocaleSelector";
import { Titlebar } from "../../components/native/Titlebar";
import { APP_VERSION } from "../../version"; import { APP_VERSION } from "../../version";
import background from "./background.jpg"; import background from "./background.jpg";
import { FormCreate } from "./forms/FormCreate"; import { FormCreate } from "./forms/FormCreate";
...@@ -23,52 +24,57 @@ export default function Login() { ...@@ -23,52 +24,57 @@ export default function Login() {
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>
); );
} }
...@@ -20,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) { ...@@ -20,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) {
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 type="spinner" />; return <Preloader type="spinner" />;
......
...@@ -63,7 +63,7 @@ export function Form({ page, callback }: Props) { ...@@ -63,7 +63,7 @@ export function Form({ page, callback }: Props) {
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);
......
...@@ -19,7 +19,11 @@ export function FormLogin() { ...@@ -19,7 +19,11 @@ export function FormLogin() {
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";
} }
......
...@@ -4,15 +4,24 @@ import { Text } from "preact-i18n"; ...@@ -4,15 +4,24 @@ import { Text } from "preact-i18n";
export function Legal() { export function Legal() {
return ( return (
<span className={styles.footer}> <span className={styles.footer}>
<a href="https://revolt.chat/about" target="_blank"> <a
href="https://revolt.chat/about"
target="_blank"
rel="noreferrer">
<Text id="general.about" /> <Text id="general.about" />
</a> </a>
&middot; &middot;
<a href="https://revolt.chat/terms" target="_blank"> <a
href="https://revolt.chat/terms"
target="_blank"
rel="noreferrer">
<Text id="general.tos" /> <Text id="general.tos" />
</a> </a>
&middot; &middot;
<a href="https://revolt.chat/privacy" target="_blank"> <a
href="https://revolt.chat/privacy"
target="_blank"
rel="noreferrer">
<Text id="general.privacy" /> <Text id="general.privacy" />
</a> </a>
</span> </span>
......
...@@ -43,7 +43,7 @@ export function MailProvider({ email }: Props) { ...@@ -43,7 +43,7 @@ export function MailProvider({ email }: Props) {
return ( return (
<div className={styles.mailProvider}> <div className={styles.mailProvider}>
<a href={provider[1]} target="_blank"> <a href={provider[1]} target="_blank" rel="noreferrer">
<Button> <Button>
<Text <Text
id="login.open_mail_provider" id="login.open_mail_provider"
......
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular"; 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 { Text } from "preact-i18n";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks"; import { useClient } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util"; import { getChannelName } from "../../context/revoltjs/util";
import Category from "../../components/ui/Category"; import Category from "../../components/ui/Category";
...@@ -14,8 +14,11 @@ import Permissions from "./channel/Permissions"; ...@@ -14,8 +14,11 @@ import Permissions from "./channel/Permissions";
export default function ChannelSettings() { export default function ChannelSettings() {
const { channel: cid } = useParams<{ channel: string }>(); 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) return null;
if ( if (
channel.channel_type === "SavedMessages" || channel.channel_type === "SavedMessages" ||
...@@ -23,13 +26,12 @@ export default function ChannelSettings() { ...@@ -23,13 +26,12 @@ export default function ChannelSettings() {
) )
return null; return null;
const history = useHistory();
function switchPage(to?: string) { function switchPage(to?: string) {
let base_url; let base_url;
switch (channel?.channel_type) { switch (channel?.channel_type) {
case "TextChannel": case "TextChannel":
case "VoiceChannel": case "VoiceChannel":
base_url = `/server/${channel.server}/channel/${cid}/settings`; base_url = `/server/${channel.server_id}/channel/${cid}/settings`;
break; break;
default: default:
base_url = `/channel/${cid}/settings`; base_url = `/channel/${cid}/settings`;
...@@ -49,7 +51,7 @@ export default function ChannelSettings() { ...@@ -49,7 +51,7 @@ export default function ChannelSettings() {
category: ( category: (
<Category <Category
variant="uniform" variant="uniform"
text={getChannelName(ctx.client, channel, true)} text={getChannelName(channel, true)}
/> />
), ),
id: "overview", id: "overview",
...@@ -66,18 +68,20 @@ export default function ChannelSettings() { ...@@ -66,18 +68,20 @@ export default function ChannelSettings() {
), ),
}, },
]} ]}
children={[ children={
<Route path="/server/:server/channel/:channel/settings/permissions"> <Switch>
<Permissions channel={channel} /> <Route path="/server/:server/channel/:channel/settings/permissions">
</Route>, <Permissions channel={channel} />
<Route path="/channel/:channel/settings/permissions"> </Route>
<Permissions channel={channel} /> <Route path="/channel/:channel/settings/permissions">
</Route>, <Permissions channel={channel} />
</Route>
<Route path="/"> <Route>
<Overview channel={channel} /> <Overview channel={channel} />
</Route>, </Route>
]} </Switch>
}
category="channel_pages" category="channel_pages"
switchPage={switchPage} switchPage={switchPage}
defaultPage="overview" defaultPage="overview"
......
import { ArrowBack, X, XCircle } from "@styled-icons/boxicons-regular"; import { ArrowBack, X } from "@styled-icons/boxicons-regular";
import { Helmet } from "react-helmet"; 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 styles from "./Settings.module.scss";
import classNames from "classnames";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
...@@ -25,6 +26,7 @@ interface Props { ...@@ -25,6 +26,7 @@ interface Props {
id: string; id: string;
icon: Children; icon: Children;
title: Children; title: Children;
hidden?: boolean;
hideTitle?: boolean; hideTitle?: boolean;
}[]; }[];
custom?: Children; custom?: Children;
...@@ -48,13 +50,18 @@ export function GenericSettings({ ...@@ -48,13 +50,18 @@ export function GenericSettings({
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
const { page } = useParams<{ page: string }>(); const { page } = useParams<{ page: string }>();
function exitSettings() { const [closing, setClosing] = useState(false);
if (history.length > 0) { const exitSettings = useCallback(() => {
history.goBack(); if (history.length > 1) {
setClosing(true);
setTimeout(() => {
history.goBack();
}, 100);
} else { } else {
history.push("/"); history.push("/");
} }
} }, [history]);
useEffect(() => { useEffect(() => {
function keyDown(e: KeyboardEvent) { function keyDown(e: KeyboardEvent) {
...@@ -65,16 +72,21 @@ export function GenericSettings({ ...@@ -65,16 +72,21 @@ export function GenericSettings({
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, []); }, [exitSettings]);
return ( return (
<div className={styles.settings} data-mobile={isTouchscreenDevice}> <div
className={classNames(styles.settings, {
[styles.closing]: closing,
[styles.native]: window.isNative,
})}
data-mobile={isTouchscreenDevice}>
<Helmet> <Helmet>
<meta <meta
name="theme-color" name="theme-color"
content={ content={
isTouchscreenDevice isTouchscreenDevice
? theme["primary-header"] ? theme["background"]
: theme["secondary-background"] : theme["secondary-background"]
} }
/> />
...@@ -85,7 +97,10 @@ export function GenericSettings({ ...@@ -85,7 +97,10 @@ export function GenericSettings({
<> <>
{showExitButton && ( {showExitButton && (
<IconButton onClick={exitSettings}> <IconButton onClick={exitSettings}>
<X size={27} style={{marginInlineEnd: "8px"}} /> <X
size={27}
style={{ marginInlineEnd: "8px" }}
/>
</IconButton> </IconButton>
)} )}
<Text id="app.settings.title" /> <Text id="app.settings.title" />
...@@ -93,7 +108,10 @@ export function GenericSettings({ ...@@ -93,7 +108,10 @@ export function GenericSettings({
) : ( ) : (
<> <>
<IconButton onClick={() => switchPage()}> <IconButton onClick={() => switchPage()}>
<ArrowBack size={24} style={{marginInlineEnd: "10px"}} /> <ArrowBack
size={24}
style={{ marginInlineEnd: "10px" }}
/>
</IconButton> </IconButton>
<Text <Text
id={`app.settings.${category}.${page}.title`} id={`app.settings.${category}.${page}.title`}
...@@ -104,53 +122,65 @@ export function GenericSettings({ ...@@ -104,53 +122,65 @@ export function GenericSettings({
)} )}
{(!isTouchscreenDevice || typeof page === "undefined") && ( {(!isTouchscreenDevice || typeof page === "undefined") && (
<div className={styles.sidebar}> <div className={styles.sidebar}>
<div className={styles.container}> <div className={styles.scrollbox}>
{pages.map((entry, i) => ( <div className={styles.container}>
<> {pages.map((entry, i) =>
{entry.category && ( entry.hidden ? undefined : (
<Category <>
variant="uniform" {entry.category && (
text={entry.category} <Category
/> variant="uniform"
)} text={entry.category}
<ButtonItem />
active={ )}
page === entry.id || <ButtonItem
(i === 0 && active={
!isTouchscreenDevice && page === entry.id ||
typeof page === "undefined") (i === 0 &&
} !isTouchscreenDevice &&
onClick={() => switchPage(entry.id)} typeof page === "undefined")
compact> }
{entry.icon} {entry.title} onClick={() => switchPage(entry.id)}
</ButtonItem> compact>
{entry.divider && <LineDivider />} {entry.icon} {entry.title}
</> </ButtonItem>
))} {entry.divider && <LineDivider />}
{custom} </>
),
)}
{custom}
</div>
</div> </div>
</div> </div>
)} )}
{(!isTouchscreenDevice || typeof page === "string") && ( {(!isTouchscreenDevice || typeof page === "string") && (
<div className={styles.content}> <div className={styles.content}>
{!isTouchscreenDevice && <div className={styles.scrollbox}>
!pages.find((x) => x.id === page && x.hideTitle) && ( <div className={styles.contentcontainer}>
<h1> {!isTouchscreenDevice &&
<Text !pages.find(
id={`app.settings.${category}.${ (x) => x.id === page && x.hideTitle,
page ?? defaultPage ) && (
}.title`} <h1>
/> <Text
</h1> 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>
</div>
)}
{!isTouchscreenDevice && (
<div className={styles.action}>
<IconButton onClick={exitSettings}>
<XCircle size={48} />
</IconButton>
</div> </div>
)} )}
</div> </div>
......
import { import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
ListUl, import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
Share, import { observer } from "mobx-react-lite";
Group, import { Route, Switch, useHistory, useParams } from "react-router-dom";
ListCheck,
} from "@styled-icons/boxicons-regular";
import { XSquare } from "@styled-icons/boxicons-solid";
import { Route, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import { useServer } from "../../context/revoltjs/hooks"; import { useClient } from "../../context/revoltjs/RevoltClient";
import Category from "../../components/ui/Category"; import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings"; import { GenericSettings } from "./GenericSettings";
import { Bans } from "./server/Bans"; import { Bans } from "./server/Bans";
import { Categories } from "./server/Categories";
import { Invites } from "./server/Invites"; import { Invites } from "./server/Invites";
import { Members } from "./server/Members"; import { Members } from "./server/Members";
import { Overview } from "./server/Overview"; import { Overview } from "./server/Overview";
import { Roles } from "./server/Roles"; import { Roles } from "./server/Roles";
export default function ServerSettings() { export default observer(() => {
const { server: sid } = useParams<{ server: string }>(); const { server: sid } = useParams<{ server: string }>();
const server = useServer(sid); const client = useClient();
const server = client.servers.get(sid);
if (!server) return null; if (!server) return null;
const history = useHistory(); const history = useHistory();
...@@ -39,13 +37,20 @@ export default function ServerSettings() { ...@@ -39,13 +37,20 @@ export default function ServerSettings() {
<GenericSettings <GenericSettings
pages={[ 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", id: "overview",
icon: <ListUl size={20} />, icon: <ListUl size={20} />,
title: ( title: (
<Text id="app.settings.server_pages.overview.title" /> <Text id="app.settings.server_pages.overview.title" />
), ),
}, },
{
id: "categories",
icon: <ListMinus size={20} />,
title: (
<Text id="app.settings.server_pages.categories.title" />
),
},
{ {
id: "members", id: "members",
icon: <Group size={20} />, icon: <Group size={20} />,
...@@ -72,35 +77,40 @@ export default function ServerSettings() { ...@@ -72,35 +77,40 @@ export default function ServerSettings() {
hideTitle: true, hideTitle: true,
}, },
]} ]}
children={[ children={
<Route path="/server/:server/settings/members"> <Switch>
<RequiresOnline> <Route path="/server/:server/settings/categories">
<Members server={server} /> <Categories server={server} />
</RequiresOnline> </Route>
</Route>, <Route path="/server/:server/settings/members">
<Route path="/server/:server/settings/invites"> <RequiresOnline>
<RequiresOnline> <Members server={server} />
<Invites server={server} /> </RequiresOnline>
</RequiresOnline> </Route>
</Route>, <Route path="/server/:server/settings/invites">
<Route path="/server/:server/settings/bans"> <RequiresOnline>
<RequiresOnline> <Invites server={server} />
<Bans server={server} /> </RequiresOnline>
</RequiresOnline> </Route>
</Route>, <Route path="/server/:server/settings/bans">
<Route path="/server/:server/settings/roles"> <RequiresOnline>
<RequiresOnline> <Bans server={server} />
<Roles server={server} /> </RequiresOnline>
</RequiresOnline> </Route>
</Route>, <Route path="/server/:server/settings/roles">
<Route path="/"> <RequiresOnline>
<Overview server={server} /> <Roles server={server} />
</Route>, </RequiresOnline>
]} </Route>
<Route>
<Overview server={server} />
</Route>
</Switch>
}
category="server_pages" category="server_pages"
switchPage={switchPage} switchPage={switchPage}
defaultPage="overview" defaultPage="overview"
showExitButton showExitButton
/> />
); );
} });
/* Settings animations */
@keyframes open { @keyframes open {
0% {transform: scale(1.2);}; 0% {
100% {transform: scale(1);}; transform: scale(1.2);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
} }
@keyframes opacity { @keyframes close {
0% {opacity: 0;}; 0% {
20% {opacity: .5;} transform: scale(1);
50% {opacity: 1;} opacity: 1;
}
100% {
transform: scale(1.2);
opacity: 0;
}
} }
@keyframes close { @keyframes opacity {
0% {transform: scale(1); opacity: 1;}; 0% {
100% {transform: scale(1.2); opacity: 0;}; opacity: 0;
}
20% {
opacity: 0.5;
}
50% {
opacity: 1;
}
} }
/* Settings CSS */
.settings[data-mobile="true"] { .settings[data-mobile="true"] {
flex-direction: column; flex-direction: column;
background: var(--primary-header); background: var(--primary-header);
.sidebar, .content { .sidebar,
.content {
background: var(--primary-background); background: var(--primary-background);
} }
.scrollbox {
&::-webkit-scrollbar-thumb {
border-top: none;
}
}
/* Sidebar */
.sidebar { .sidebar {
justify-content: flex-start; overflow-y: auto;
.container { .container {
padding: 20px 8px; padding: 20px 8px calc(var(--bottom-navigation-height) + 30px);
min-width: 218px; min-width: 218px;
} }
> div { .scrollbox {
width: 100%; width: 100%;
} }
.version { .version {
place-items: center; place-items: center;
} }
} }
/* Content */
.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;
}
} }
} }
...@@ -52,8 +88,11 @@ ...@@ -52,8 +88,11 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
position: fixed; position: fixed;
animation: open .18s ease-out, animation: open 0.18s ease-out, opacity 0.18s;
opacity .18s;
&.closing {
animation: close 0.18s ease-in;
}
} }
.settings { .settings {
...@@ -61,20 +100,40 @@ ...@@ -61,20 +100,40 @@
display: flex; display: flex;
user-select: none; user-select: none;
flex-direction: row; flex-direction: row;
justify-content: center;
background: var(--primary-background); 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 { .sidebar {
flex: 2; flex: 1 0 218px;
display: flex; display: flex;
flex-shrink: 0;
overflow-y: scroll;
justify-content: flex-end; justify-content: flex-end;
background: var(--secondary-background); background: var(--secondary-background);
.container { .container {
width: 218px; min-width: 218px;
padding: 60px 8px; padding: 80px 8px;
display: flex;
gap: 2px;
flex-direction: column;
} }
.divider { .divider {
...@@ -84,20 +143,17 @@ ...@@ -84,20 +143,17 @@
.donate { .donate {
color: goldenrod !important; color: goldenrod !important;
} }
.logOut { .logOut {
color: var(--error) !important; color: var(--error) !important;
} }
.version { .version {
margin: 1rem 12px 0; margin: 1rem 12px 0;
font-size: 10px; font-size: 0.625rem;
color: var(--secondary-foreground); color: var(--secondary-foreground);
font-family: var(--monoscape-font), monospace; font-family: var(--monospace-font), monospace;
user-select: text; user-select: text;
display: grid; display: grid;
//place-items: center;
> div { > div {
gap: 2px; gap: 2px;
...@@ -105,87 +161,118 @@ ...@@ -105,87 +161,118 @@
flex-direction: column; flex-direction: column;
} }
.revision a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
} }
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
} }
.content { .content {
flex: 3; flex: 1 1 800px;
max-width: 740px; display: flex;
padding: 60px 2em; overflow-y: auto;
overflow-y: scroll;
overflow-x: hidden; .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 { details {
margin: 14px 0; margin: 14px 0;
} }
h1 { h1 {
margin-top: 0; margin: 0;
line-height: 1em; line-height: 1rem;
font-size: 1.2em; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
} }
h3 { h3 {
font-size: 13px; font-size: 0.8125rem;
text-transform: uppercase; text-transform: uppercase;
color: var(--secondary-foreground); color: var(--secondary-foreground);
&:first-child {
margin-top: 0;
}
} }
h4 { h4 {
margin: 4px 2px; margin: 4px 2px;
font-size: 13px; font-size: 0.8125rem;
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
text-transform: uppercase; text-transform: uppercase;
} }
h5 {
margin-top: 0;
font-size: 0.75rem;
font-weight: 400;
}
.footer { .footer {
border-top: 1px solid; border-top: 1px solid;
margin: 0; margin: 0;
padding-top: 5px; padding-top: 5px;
font-size: 14px; font-size: 0.875rem;
color: var(--secondary-foreground); color: var(--secondary-foreground);
} }
} }
.action { .action {
flex: 1; flex-grow: 1;
flex-shrink: 0; padding: 80px 8px;
padding: 60px 8px; visibility: visible;
color: var(--tertiary-background); position: sticky;
top: 0;
&:after { &:after {
content: "ESC"; content: "ESC";
margin-top: 4px;
display: flex; display: flex;
text-align: center;
align-content: center;
justify-content: center; justify-content: center;
position: relative; width: 40px;
color: var(--foreground); opacity: 0.5;
width: 48px; font-size: 0.75rem;
opacity: .5; }
font-size: .75em;
} .closeButton {
display: flex;
> div { align-items: center;
display: inline; justify-content: center;
> svg { border-radius: 50%;
&:active { height: 40px;
transform: translateY(2px); width: 40px;
} border: 3px solid var(--tertiary-background);
cursor: pointer;
svg {
color: var(--secondary-foreground);
}
&:hover {
background: var(--secondary-header);
}
&:active {
transform: translateY(2px);
} }
} }
} }
} }
.loader { @media (pointer: coarse) {
> div { .scrollbox {
margin: auto; visibility: visible !important;
overflow-y: auto;
} }
} }
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
Sync as SyncIcon, Sync as SyncIcon,
Globe, Globe,
LogOut, LogOut,
Desktop,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { import {
Bell, Bell,
...@@ -14,7 +15,7 @@ import { ...@@ -14,7 +15,7 @@ import {
User, User,
Megaphone, Megaphone,
} from "@styled-icons/boxicons-solid"; } 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 { LIBRARY_VERSION } from "revolt.js";
import styles from "./Settings.module.scss"; import styles from "./Settings.module.scss";
...@@ -38,6 +39,7 @@ import { Appearance } from "./panes/Appearance"; ...@@ -38,6 +39,7 @@ import { Appearance } from "./panes/Appearance";
import { ExperimentsPage } from "./panes/Experiments"; import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback"; import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages"; import { Languages } from "./panes/Languages";
import { Native } from "./panes/Native";
import { Notifications } from "./panes/Notifications"; import { Notifications } from "./panes/Notifications";
import { Profile } from "./panes/Profile"; import { Profile } from "./panes/Profile";
import { Sessions } from "./panes/Sessions"; import { Sessions } from "./panes/Sessions";
...@@ -100,6 +102,12 @@ export default function Settings() { ...@@ -100,6 +102,12 @@ export default function Settings() {
icon: <SyncIcon size={20} />, icon: <SyncIcon size={20} />,
title: <Text id="app.settings.pages.sync.title" />, 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, divider: true,
id: "experiments", id: "experiments",
...@@ -112,67 +120,79 @@ export default function Settings() { ...@@ -112,67 +120,79 @@ export default function Settings() {
title: <Text id="app.settings.pages.feedback.title" />, title: <Text id="app.settings.pages.feedback.title" />,
}, },
]} ]}
children={[ children={
<Route path="/settings/profile"> <Switch>
<Profile /> <Route path="/settings/profile">
</Route>, <Profile />
<Route path="/settings/sessions"> </Route>
<RequiresOnline> <Route path="/settings/sessions">
<Sessions /> <RequiresOnline>
</RequiresOnline> <Sessions />
</Route>, </RequiresOnline>
<Route path="/settings/appearance"> </Route>
<Appearance /> <Route path="/settings/appearance">
</Route>, <Appearance />
<Route path="/settings/notifications"> </Route>
<Notifications /> <Route path="/settings/notifications">
</Route>, <Notifications />
<Route path="/settings/language"> </Route>
<Languages /> <Route path="/settings/language">
</Route>, <Languages />
<Route path="/settings/sync"> </Route>
<Sync /> <Route path="/settings/sync">
</Route>, <Sync />
<Route path="/settings/experiments"> </Route>
<ExperimentsPage /> <Route path="/settings/native">
</Route>, <Native />
<Route path="/settings/feedback"> </Route>
<Feedback /> <Route path="/settings/experiments">
</Route>, <ExperimentsPage />
<Route path="/"> </Route>
<Account /> <Route path="/settings/feedback">
</Route>, <Feedback />
]} </Route>
<Route path="/">
<Account />
</Route>
</Switch>
}
defaultPage="account" defaultPage="account"
switchPage={switchPage} switchPage={switchPage}
category="pages" category="pages"
custom={[ custom={
<a href="https://gitlab.insrt.uk/revolt" target="_blank"> <>
<ButtonItem compact> <a
<Gitlab size={20} /> href="https://gitlab.insrt.uk/revolt"
<Text id="app.settings.pages.source_code" /> 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> </ButtonItem>
</a>, <div className={styles.version}>
<a href="https://ko-fi.com/insertish" target="_blank">
<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>
<span className={styles.revision}> <span className={styles.revision}>
<a <a
href={`${REPO_URL}/${GIT_REVISION}`} href={`${REPO_URL}/${GIT_REVISION}`}
target="_blank"> target="_blank"
rel="noreferrer">
{GIT_REVISION.substr(0, 7)} {GIT_REVISION.substr(0, 7)}
</a> </a>
{` `} {` `}
...@@ -182,7 +202,8 @@ export default function Settings() { ...@@ -182,7 +202,8 @@ export default function Settings() {
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}` ? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
: undefined : undefined
} }
target="_blank"> target="_blank"
rel="noreferrer">
({GIT_BRANCH}) ({GIT_BRANCH})
</a> </a>
</span> </span>
...@@ -190,13 +211,16 @@ export default function Settings() { ...@@ -190,13 +211,16 @@ export default function Settings() {
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "} {GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
{APP_VERSION} {APP_VERSION}
</span> </span>
{window.isNative && (
<span>Native: {window.nativeVersion}</span>
)}
<span> <span>
API: {client.configuration?.revolt ?? "N/A"} API: {client.configuration?.revolt ?? "N/A"}
</span> </span>
<span>revolt.js: {LIBRARY_VERSION}</span> <span>revolt.js: {LIBRARY_VERSION}</span>
</div> </div>
</div>, </>
]} }
/> />
); );
} }