Compare commits
83 Commits
feat/dragg
...
1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95ebd935ed | ||
|
|
3b7c1cbe20 | ||
|
|
cb0a521473 | ||
|
|
aa9974149c | ||
|
|
569864167e | ||
|
|
f185dec461 | ||
|
|
03e177f865 | ||
|
|
f685352963 | ||
|
|
6755217ad2 | ||
|
|
211ff2058a | ||
|
|
b7be9f8c03 | ||
|
|
241b9cd27b | ||
|
|
d10bd96900 | ||
|
|
374be319c4 | ||
|
|
0ee7b73d61 | ||
|
|
63d5f6bb7d | ||
|
|
34e6995d86 | ||
|
|
a1ef1dce5e | ||
|
|
a190a51d0b | ||
|
|
c9127d6cf3 | ||
|
|
f0d2e31b17 | ||
|
|
220a28a151 | ||
|
|
b44779c89a | ||
|
|
5835064219 | ||
|
|
b9da79bc11 | ||
|
|
ba99cbaf2a | ||
|
|
fc0c7611d4 | ||
|
|
2e9c013ed8 | ||
|
|
3e40a61624 | ||
|
|
4e22ccb2f7 | ||
|
|
00718245f9 | ||
|
|
cd8ab6739b | ||
|
|
5eabd2861f | ||
|
|
a404ff7fe0 | ||
|
|
7680931f5f | ||
|
|
c1324108e3 | ||
|
|
56770d40df | ||
|
|
64f19ec2c0 | ||
|
|
dbb1c1e8fa | ||
|
|
c686e85d37 | ||
|
|
8eefc87b05 | ||
|
|
645e1af6db | ||
|
|
8103cc03cf | ||
|
|
bdc527ebbe | ||
|
|
8a2826da91 | ||
|
|
bd50378234 | ||
|
|
71f8fc86a4 | ||
|
|
277eaa685d | ||
|
|
e81b8ed472 | ||
|
|
6be0807433 | ||
|
|
e0ca1681bd | ||
|
|
ebcbe4bd4b | ||
|
|
ec8b51f559 | ||
|
|
72e60b7528 | ||
|
|
3399991824 | ||
|
|
6497e11fb0 | ||
|
|
7544c78360 | ||
|
|
040c4367f7 | ||
|
|
ee80dfd3c8 | ||
|
|
67c8418c31 | ||
|
|
2056232759 | ||
|
|
81bf325990 | ||
|
|
906f15f103 | ||
|
|
ea106a3902 | ||
|
|
41e533ab59 | ||
|
|
68b9d5ea79 | ||
|
|
673efc0586 | ||
|
|
74f8c552ed | ||
|
|
f3bdbe52d9 | ||
|
|
b4777e9816 | ||
|
|
ab77d4a812 | ||
|
|
1d243d4762 | ||
|
|
2f9bfbf83f | ||
|
|
c2547b3ead | ||
|
|
a64fe61199 | ||
|
|
1bd138d6ef | ||
|
|
4bcfa601a5 | ||
|
|
20d31babce | ||
|
|
12b9716043 | ||
|
|
e2d9e41a58 | ||
|
|
bdf741e0ee | ||
|
|
588cb7c019 | ||
|
|
94dd4b464a |
2
.github/workflows/docker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "*"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "!.github/workflows/docker.yml"
|
- "!.github/workflows/docker.yml"
|
||||||
|
|||||||
1
.github/workflows/mirroring.yml
vendored
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
- "production"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
to_gitlab:
|
to_gitlab:
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
image: node:16-buster
|
|
||||||
|
|
||||||
variables:
|
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
|
|
||||||
# Fetch dependencies and setup project for compilation.
|
|
||||||
install:
|
|
||||||
stage: prepare
|
|
||||||
script:
|
|
||||||
- yarn
|
|
||||||
|
|
||||||
# Type check the project
|
|
||||||
typecheck:
|
|
||||||
stage: test
|
|
||||||
needs:
|
|
||||||
- install
|
|
||||||
dependencies:
|
|
||||||
- install
|
|
||||||
script:
|
|
||||||
- yarn typecheck
|
|
||||||
|
|
||||||
# Lint the project and check prettier output.
|
|
||||||
lint:
|
|
||||||
stage: test
|
|
||||||
allow_failure: true
|
|
||||||
needs:
|
|
||||||
- install
|
|
||||||
dependencies:
|
|
||||||
- install
|
|
||||||
script:
|
|
||||||
- yarn lint
|
|
||||||
- yarn --check 'src/**/*.{js,jsx,ts,tsx}'
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- prepare
|
|
||||||
- test
|
|
||||||
13
README.md
@@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
|
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
|
||||||
|
|
||||||
|
## Pending Rewrite
|
||||||
|
|
||||||
|
The following code is pending a partial or full rewrite:
|
||||||
|
|
||||||
|
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||||
|
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||||
|
- `src/context/intermediate`: modal system is being rewritten from scratch
|
||||||
|
- `src/context/revoltjs`: client state management needs to be rewritten and include support for concurrent clients
|
||||||
|
- `src/lib`: this needs to be organised
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- [Preact](https://preactjs.com/)
|
- [Preact](https://preactjs.com/)
|
||||||
@@ -43,7 +53,7 @@ You can now access the client at http://local.revolt.chat:3000.
|
|||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------- | -------------------------------------------- |
|
| --------------------------------------- | -------------------------------------------- |
|
||||||
| `yarn pull` | Setup assets required for Revite. |
|
| `yarn pull` | Setup assets required for Revite. |
|
||||||
| `yarn dev` | Start the Revolt client in development mode. |
|
| `yarn dev` | Start the Revolt client in development mode. |
|
||||||
| `yarn build` | Build the Revolt client. |
|
| `yarn build` | Build the Revolt client. |
|
||||||
@@ -53,6 +63,7 @@ You can now access the client at http://local.revolt.chat:3000.
|
|||||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||||
| `yarn start` | Start a local sirv server with built client. |
|
| `yarn start` | Start a local sirv server with built client. |
|
||||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
| `yarn start:inject` | Inject a given API URL and start server. |
|
||||||
|
| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
2
external/lang
vendored
32
package.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.0",
|
"version": "1.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/setup_assets.js --check && vite",
|
"dev": "node scripts/setup_assets.js --check && vite",
|
||||||
"pull": "node scripts/setup_assets.js",
|
"pull": "node scripts/setup_assets.js",
|
||||||
@@ -39,24 +39,7 @@
|
|||||||
"varsIgnorePattern": "^_"
|
"varsIgnorePattern": "^_"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require-jsdoc": [
|
"react/jsx-no-literals": "warn"
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"require": {
|
|
||||||
"FunctionDeclaration": true,
|
|
||||||
"MethodDefinition": true,
|
|
||||||
"ClassDeclaration": true,
|
|
||||||
"ArrowFunctionExpression": false,
|
|
||||||
"FunctionExpression": false
|
|
||||||
},
|
|
||||||
"ignore": {
|
|
||||||
"MethodDefinition": [
|
|
||||||
"toJSON",
|
|
||||||
"hydrate"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -90,7 +73,7 @@
|
|||||||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
||||||
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
||||||
"@preact/preset-vite": "^2.0.0",
|
"@preact/preset-vite": "^2.0.0",
|
||||||
"@revoltchat/ui": "1.0.33",
|
"@revoltchat/ui": "1.0.46",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@styled-icons/boxicons-logos": "^10.38.0",
|
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||||
"@styled-icons/boxicons-regular": "^10.38.0",
|
"@styled-icons/boxicons-regular": "^10.38.0",
|
||||||
@@ -110,6 +93,7 @@
|
|||||||
"@types/react-helmet": "^6.1.1",
|
"@types/react-helmet": "^6.1.1",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-scroll": "^1.8.2",
|
"@types/react-scroll": "^1.8.2",
|
||||||
|
"@types/semver": "^7",
|
||||||
"@types/styled-components": "^5.1.10",
|
"@types/styled-components": "^5.1.10",
|
||||||
"@types/twemoji": "^12.1.1",
|
"@types/twemoji": "^12.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||||
@@ -121,7 +105,9 @@
|
|||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
|
"eslint-plugin-jsdoc": "^39.3.2",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
|
"history": "4",
|
||||||
"json-stringify-deterministic": "^1.0.2",
|
"json-stringify-deterministic": "^1.0.2",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"lodash.defaultsdeep": "^4.6.1",
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
@@ -133,10 +119,11 @@
|
|||||||
"mobx": "^6.6.0",
|
"mobx": "^6.6.0",
|
||||||
"mobx-react-lite": "3.4.0",
|
"mobx-react-lite": "3.4.0",
|
||||||
"preact": "^10.5.14",
|
"preact": "^10.5.14",
|
||||||
"preact-context-menu": "0.4.0-patch.0",
|
"preact-context-menu": "0.4.1",
|
||||||
"preact-i18n": "^2.4.0-preactx",
|
"preact-i18n": "^2.4.0-preactx",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
|
"qrcode.react": "^3.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-device-detect": "2.2.2",
|
"react-device-detect": "2.2.2",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
@@ -145,9 +132,10 @@
|
|||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scroll": "^1.8.2",
|
"react-scroll": "^1.8.2",
|
||||||
"react-virtuoso": "^2.12.0",
|
"react-virtuoso": "^2.12.0",
|
||||||
"revolt.js": "6.0.1",
|
"revolt.js": "6.0.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
|
"semver": "^7.3.7",
|
||||||
"shade-blend-color": "^1.0.0",
|
"shade-blend-color": "^1.0.0",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
|
|||||||
38
src/assets/changelogs.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
type Element =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
type: "image";
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ChangelogPost {
|
||||||
|
date: Date;
|
||||||
|
title: string;
|
||||||
|
content: Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changelogEntries: Record<number, ChangelogPost> = {
|
||||||
|
1: {
|
||||||
|
date: new Date("2022-06-12T20:39:16.674Z"),
|
||||||
|
title: "Secure your account with 2FA",
|
||||||
|
content: [
|
||||||
|
"Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.",
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png",
|
||||||
|
},
|
||||||
|
"Once enabled, you will be prompted on login.",
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png",
|
||||||
|
},
|
||||||
|
"Other authentication methods coming later, stay tuned!",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changelogEntryArray = Object.keys(changelogEntries).map(
|
||||||
|
(index) => changelogEntries[index as unknown as number],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const latestChangelog = changelogEntryArray.length;
|
||||||
@@ -1850,7 +1850,7 @@ export const emojiDictionary = {
|
|||||||
scotland: "🏴",
|
scotland: "🏴",
|
||||||
wales: "🏴",
|
wales: "🏴",
|
||||||
...{
|
...{
|
||||||
"1984": "custom:1984.gif",
|
1984: "custom:1984.gif",
|
||||||
KekW: "custom:KekW.png",
|
KekW: "custom:KekW.png",
|
||||||
amogus: "custom:amogus.gif",
|
amogus: "custom:amogus.gif",
|
||||||
awaa: "custom:awaa.png",
|
awaa: "custom:awaa.png",
|
||||||
|
|||||||
14
src/components/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
The following folders should not be added to or modified:
|
||||||
|
|
||||||
|
- `common`
|
||||||
|
- `markdown`
|
||||||
|
- `native`
|
||||||
|
- `ui`
|
||||||
|
|
||||||
|
The following are part-legacy, will remain in place and will be rewritten to some degree still:
|
||||||
|
|
||||||
|
- `navigation`
|
||||||
|
|
||||||
|
The following are mostly good to go:
|
||||||
|
|
||||||
|
- `settings`
|
||||||
@@ -6,15 +6,11 @@ import styled from "styled-components/macro";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import { Button, Checkbox } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
||||||
|
|
||||||
import Checkbox from "../ui/Checkbox";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -81,10 +77,10 @@ export default observer((props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={layout.getSectionState(SECTION_NSFW, false)}
|
title={<Text id="app.main.channel.nsfw.confirm" />}
|
||||||
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}>
|
value={layout.getSectionState(SECTION_NSFW, false)}
|
||||||
<Text id="app.main.channel.nsfw.confirm" />
|
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}
|
||||||
</Checkbox>
|
/>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Button palette="secondary" onClick={() => history.goBack()}>
|
<Button palette="secondary" onClick={() => history.goBack()}>
|
||||||
<Text id="app.special.modals.actions.back" />
|
<Text id="app.special.modals.actions.back" />
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function useAutoComplete(
|
|||||||
? "emoji"
|
? "emoji"
|
||||||
: "user",
|
: "user",
|
||||||
search.toLowerCase(),
|
search.toLowerCase(),
|
||||||
j + 1,
|
current === ":" ? j + 1 : j,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ export function useAutoComplete(
|
|||||||
);
|
);
|
||||||
} else if (state.type === "user") {
|
} else if (state.type === "user") {
|
||||||
content.splice(
|
content.splice(
|
||||||
index - 1,
|
index,
|
||||||
search.length + 1,
|
search.length + 1,
|
||||||
"<@",
|
"<@",
|
||||||
state.matches[state.selected]._id,
|
state.matches[state.selected]._id,
|
||||||
@@ -250,7 +250,7 @@ export function useAutoComplete(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content.splice(
|
content.splice(
|
||||||
index - 1,
|
index,
|
||||||
search.length + 1,
|
search.length + 1,
|
||||||
"<#",
|
"<#",
|
||||||
state.matches[state.selected]._id,
|
state.matches[state.selected]._id,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
|
import { Details } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import Details from "../ui/Details";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
defaultValue: boolean;
|
defaultValue: boolean;
|
||||||
@@ -34,7 +32,7 @@ export default function CollapsibleSection({
|
|||||||
}
|
}
|
||||||
{...detailsProps}>
|
{...detailsProps}>
|
||||||
<summary>
|
<summary>
|
||||||
<div class="padding">
|
<div className="padding">
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
{summary}
|
{summary}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useApplicationState } from "../../mobx/State";
|
import { ComboBox } from "@revoltchat/ui";
|
||||||
|
|
||||||
import ComboBox from "../ui/ComboBox";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { Language, Languages } from "../../../external/lang/Languages";
|
import { Language, Languages } from "../../../external/lang/Languages";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import styled, { css } from "styled-components/macro";
|
|||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import IconButton from "../ui/IconButton";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import styled from "styled-components/macro";
|
|||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
type Props = Omit<TippyProps, "children"> & {
|
type Props = Omit<TippyProps, "children"> & {
|
||||||
children: Children;
|
children: Children;
|
||||||
content: Children;
|
content: Children;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
|||||||
|
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import IconButton from "../ui/IconButton";
|
import { updateSW } from "../../updateWorker";
|
||||||
|
|
||||||
import { updateSW } from "../../main";
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
let pendingUpdate = false;
|
let pendingUpdate = false;
|
||||||
@@ -31,7 +31,7 @@ export default function UpdateIndicator({ style }: Props) {
|
|||||||
|
|
||||||
if (style === "titlebar") {
|
if (style === "titlebar") {
|
||||||
return (
|
return (
|
||||||
<div class="actions">
|
<div className="actions">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="A new update is available!"
|
content="A new update is available!"
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
@@ -46,7 +46,7 @@ export default function UpdateIndicator({ style }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.isNative) return null;
|
if (window.isNative && window.native.getConfig().frame) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={() => updateSW(true)}>
|
<IconButton onClick={() => updateSW(true)}>
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import { useTriggerEvents } from "preact-context-menu";
|
|||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
|
import { I18nError } from "../../../context/Locale";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Overline from "../../ui/Overline";
|
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
import Markdown from "../../markdown/Markdown";
|
import Markdown from "../../markdown/Markdown";
|
||||||
import UserIcon from "../user/UserIcon";
|
import UserIcon from "../user/UserIcon";
|
||||||
import { Username } from "../user/UserShort";
|
import { Username } from "../user/UserShort";
|
||||||
@@ -162,7 +162,9 @@ const Message = observer(
|
|||||||
{replacement ?? <Markdown content={content} />}
|
{replacement ?? <Markdown content={content} />}
|
||||||
{!queued && <InviteList message={message} />}
|
{!queued && <InviteList message={message} />}
|
||||||
{queued?.error && (
|
{queued?.error && (
|
||||||
<Overline type="error" error={queued.error} />
|
<Category>
|
||||||
|
<I18nError error={queued.error} />
|
||||||
|
</Category>
|
||||||
)}
|
)}
|
||||||
{message.attachments?.map((attachment, index) => (
|
{message.attachments?.map((attachment, index) => (
|
||||||
<Attachment
|
<Attachment
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||||
import Axios, { CancelTokenSource } from "axios";
|
import Axios, { CancelTokenSource } from "axios";
|
||||||
import Long from "long";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import {
|
import { Channel } from "revolt.js";
|
||||||
Channel,
|
|
||||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
|
||||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
|
||||||
Permission,
|
|
||||||
Server,
|
|
||||||
U32_MAX,
|
|
||||||
UserPermission,
|
|
||||||
} from "revolt.js";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
import { debounce } from "../../../lib/debounce";
|
import { debounce } from "../../../lib/debounce";
|
||||||
import { defer } from "../../../lib/defer";
|
import { defer } from "../../../lib/defer";
|
||||||
@@ -31,7 +24,7 @@ import {
|
|||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { modalController } from "../../../context/modals";
|
||||||
import {
|
import {
|
||||||
FileUploader,
|
FileUploader,
|
||||||
grabFiles,
|
grabFiles,
|
||||||
@@ -40,8 +33,6 @@ import {
|
|||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
import IconButton from "../../ui/IconButton";
|
|
||||||
|
|
||||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||||
import { PermissionTooltip } from "../Tooltip";
|
import { PermissionTooltip } from "../Tooltip";
|
||||||
import FilePreview from "./bars/FilePreview";
|
import FilePreview from "./bars/FilePreview";
|
||||||
@@ -104,7 +95,7 @@ const Blocked = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Action = styled.div`
|
const Action = styled.div`
|
||||||
> div {
|
> a {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -127,7 +118,7 @@ const Action = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const FileAction = styled.div`
|
const FileAction = styled.div`
|
||||||
> div {
|
> a {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: 62px;
|
width: 62px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -157,7 +148,6 @@ export default observer(({ channel }: Props) => {
|
|||||||
});
|
});
|
||||||
const [typing, setTyping] = useState<boolean | number>(false);
|
const [typing, setTyping] = useState<boolean | number>(false);
|
||||||
const [replies, setReplies] = useState<Reply[]>([]);
|
const [replies, setReplies] = useState<Reply[]>([]);
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
|
||||||
@@ -482,7 +472,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
files: [...uploadState.files, ...files],
|
files: [...uploadState.files, ...files],
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
openScreen({ id: "error", error: "FileTooLarge" }),
|
modalController.push({
|
||||||
|
type: "error",
|
||||||
|
error: "FileTooLarge",
|
||||||
|
}),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import styles from "./AttachmentActions.module.scss";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { determineFileSize } from "../../../../lib/fileSize";
|
import { determineFileSize } from "../../../../lib/fileSize";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import IconButton from "../../../ui/IconButton";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import styled from "styled-components/macro";
|
|||||||
|
|
||||||
import { Ref } from "preact";
|
import { Ref } from "preact";
|
||||||
|
|
||||||
import { Children } from "../../../../types/Preact";
|
|
||||||
|
|
||||||
const Grid = styled.div<{ width: number; height: number }>`
|
const Grid = styled.div<{ width: number; height: number }>`
|
||||||
--width: ${(props) => props.width}px;
|
--width: ${(props) => props.width}px;
|
||||||
--height: ${(props) => props.height}px;
|
--height: ${(props) => props.height}px;
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import styles from "./Attachment.module.scss";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
AppContext,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
} from "../../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Preloader from "../../../ui/Preloader";
|
|
||||||
import { Button } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,12 +149,12 @@ function FileEntry({
|
|||||||
<EmptyEntry className="icon">
|
<EmptyEntry className="icon">
|
||||||
<File size={36} />
|
<File size={36} />
|
||||||
</EmptyEntry>
|
</EmptyEntry>
|
||||||
<div class="overlay">
|
<div className="overlay">
|
||||||
<XCircle size={36} />
|
<XCircle size={36} />
|
||||||
</div>
|
</div>
|
||||||
</PreviewBox>
|
</PreviewBox>
|
||||||
<span class="fn">{file.name}</span>
|
<span className="fn">{file.name}</span>
|
||||||
<span class="size">{determineFileSize(file.size)}</span>
|
<span className="size">{determineFileSize(file.size)}</span>
|
||||||
</Entry>
|
</Entry>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,13 +169,18 @@ function FileEntry({
|
|||||||
return (
|
return (
|
||||||
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
||||||
<PreviewBox onClick={remove}>
|
<PreviewBox onClick={remove}>
|
||||||
<img class="icon" src={url} alt={file.name} loading="eager" />
|
<img
|
||||||
<div class="overlay">
|
className="icon"
|
||||||
|
src={url}
|
||||||
|
alt={file.name}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<div className="overlay">
|
||||||
<XCircle size={36} />
|
<XCircle size={36} />
|
||||||
</div>
|
</div>
|
||||||
</PreviewBox>
|
</PreviewBox>
|
||||||
<span class="fn">{file.name}</span>
|
<span className="fn">{file.name}</span>
|
||||||
<span class="size">{determineFileSize(file.size)}</span>
|
<span className="size">{determineFileSize(file.size)}</span>
|
||||||
</Entry>
|
</Entry>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import styled from "styled-components/macro";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { StateUpdater, useEffect } from "preact/hooks";
|
import { StateUpdater, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../../mobx/State";
|
import { useApplicationState } from "../../../../mobx/State";
|
||||||
@@ -14,8 +16,6 @@ import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
|
|||||||
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import Tooltip from "../../../common/Tooltip";
|
import Tooltip from "../../../common/Tooltip";
|
||||||
import IconButton from "../../../ui/IconButton";
|
|
||||||
|
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import UserShort from "../../user/UserShort";
|
import UserShort from "../../user/UserShort";
|
||||||
import { SystemMessage } from "../SystemMessage";
|
import { SystemMessage } from "../SystemMessage";
|
||||||
@@ -152,11 +152,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Base key={reply.id}>
|
<Base key={reply.id}>
|
||||||
<ReplyBase preview>
|
<ReplyBase preview>
|
||||||
<div class="replyto">
|
<div className="replyto">
|
||||||
<Text id="app.main.channel.reply.replying" />
|
<Text id="app.main.channel.reply.replying" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div className="content">
|
||||||
<div class="username">
|
<div className="username">
|
||||||
<UserShort
|
<UserShort
|
||||||
size={16}
|
size={16}
|
||||||
showServerIdentity
|
showServerIdentity
|
||||||
@@ -164,7 +164,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
masquerade={message.masquerade!}
|
masquerade={message.masquerade!}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="message">
|
<div className="message">
|
||||||
{message.attachments && (
|
{message.attachments && (
|
||||||
<>
|
<>
|
||||||
<File size={16} />
|
<File size={16} />
|
||||||
@@ -196,7 +196,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ReplyBase>
|
</ReplyBase>
|
||||||
<span class="actions">
|
<span className="actions">
|
||||||
{message.author_id !== client.user!._id && (
|
{message.author_id !== client.user!._id && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -225,7 +225,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
content={
|
content={
|
||||||
<Text id="app.main.channel.reply.toggle" />
|
<Text id="app.main.channel.reply.toggle" />
|
||||||
}>
|
}>
|
||||||
<span class="toggle">
|
<span className="toggle">
|
||||||
<At size={15} />
|
<At size={15} />
|
||||||
<Text
|
<Text
|
||||||
id={
|
id={
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import styled, { css } from "styled-components/macro";
|
|||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
|
import { I18nError } from "../../../../context/Locale";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
AppContext,
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
@@ -19,8 +20,6 @@ import {
|
|||||||
import { takeError } from "../../../../context/revoltjs/util";
|
import { takeError } from "../../../../context/revoltjs/util";
|
||||||
|
|
||||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||||
import Overline from "../../../ui/Overline";
|
|
||||||
import Preloader from "../../../ui/Preloader";
|
|
||||||
|
|
||||||
const EmbedInviteBase = styled.div`
|
const EmbedInviteBase = styled.div`
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@@ -160,7 +159,11 @@ export function EmbedInvite({ code }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</EmbedInviteBase>
|
</EmbedInviteBase>
|
||||||
{joinError && <Overline type="error" error={joinError} />}
|
{joinError && (
|
||||||
|
<Category>
|
||||||
|
<I18nError error={joinError} />
|
||||||
|
</Category>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { API } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import IconButton from "../../../ui/IconButton";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: API.Image;
|
embed: API.Image;
|
||||||
@@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href={embed.url}
|
href={embed.url}
|
||||||
class={styles.openIcon}
|
className={styles.openIcon}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { User } from "revolt.js";
|
import { User } from "revolt.js";
|
||||||
|
|
||||||
import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
|
import { Checkbox, Row, Column } from "@revoltchat/ui";
|
||||||
|
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
import { Username } from "./UserShort";
|
import { Username } from "./UserShort";
|
||||||
|
|
||||||
type UserProps = Omit<CheckboxProps, "children"> & { user: User };
|
type UserProps = { value: boolean; onChange: (v: boolean) => void; user: User };
|
||||||
|
|
||||||
export default function UserCheckbox({ user, ...props }: UserProps) {
|
export default function UserCheckbox({ user, ...props }: UserProps) {
|
||||||
return (
|
return (
|
||||||
<Checkbox {...props}>
|
<Checkbox
|
||||||
|
{...props}
|
||||||
|
title={
|
||||||
|
<Row centred>
|
||||||
<UserIcon target={user} size={32} />
|
<UserIcon target={user} size={32} />
|
||||||
|
<Column centred>
|
||||||
<Username user={user} />
|
<Username user={user} />
|
||||||
</Checkbox>
|
</Column>
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import styled from "styled-components/macro";
|
|||||||
import { openContextMenu } from "preact-context-menu";
|
import { openContextMenu } from "preact-context-menu";
|
||||||
import { Text, Localizer } from "preact-i18n";
|
import { Text, Localizer } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Header, IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
import Header from "../../ui/Header";
|
|
||||||
import IconButton from "../../ui/IconButton";
|
|
||||||
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import UserStatus from "./UserStatus";
|
import UserStatus from "./UserStatus";
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ export default observer(({ user }: Props) => {
|
|||||||
const { writeClipboard } = useIntermediate();
|
const { writeClipboard } = useIntermediate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header topBorder placement="secondary">
|
<Header topBorder palette="secondary">
|
||||||
<HeaderBase>
|
<HeaderBase>
|
||||||
<Localizer>
|
<Localizer>
|
||||||
<Tooltip content={<Text id="app.special.copy_username" />}>
|
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { User } from "revolt.js";
|
import { User } from "revolt.js";
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import { Username } from "./UserShort";
|
import { Username } from "./UserShort";
|
||||||
import UserStatus from "./UserStatus";
|
import UserStatus from "./UserStatus";
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useClient();
|
const client = useApplicationState().client!;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
target,
|
target,
|
||||||
@@ -114,7 +114,7 @@ export default observer(
|
|||||||
y="0"
|
y="0"
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
class="icon"
|
className="icon"
|
||||||
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
||||||
{<img src={url} draggable={false} loading="lazy" />}
|
{<img src={url} draggable={false} loading="lazy" />}
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import { dayjs } from "../../context/Locale";
|
|||||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import { generateEmoji } from "../common/Emoji";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { generateEmoji } from "../common/Emoji";
|
||||||
import { MarkdownProps } from "./Markdown";
|
import { MarkdownProps } from "./Markdown";
|
||||||
import Prism from "./prism";
|
import Prism from "./prism";
|
||||||
|
|
||||||
@@ -169,7 +168,10 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
|||||||
const id = args[0] as string,
|
const id = args[0] as string,
|
||||||
channel = client.channels.get(id);
|
channel = client.channels.get(id);
|
||||||
|
|
||||||
if (channel?.channel_type === "TextChannel") {
|
if (
|
||||||
|
channel?.channel_type === "TextChannel" ||
|
||||||
|
channel?.channel_type === "VoiceChannel"
|
||||||
|
) {
|
||||||
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
|
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const TitlebarBase = styled.div<Props>`
|
|||||||
export function Titlebar(props: Props) {
|
export function Titlebar(props: Props) {
|
||||||
return (
|
return (
|
||||||
<TitlebarBase {...props}>
|
<TitlebarBase {...props}>
|
||||||
<div class="title">
|
<div className="title">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 193.733 37.438">
|
viewBox="0 0 193.733 37.438">
|
||||||
@@ -114,7 +114,7 @@ export function Titlebar(props: Props) {
|
|||||||
<Wrench size="12.5" />
|
<Wrench size="12.5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/*<div class="actions quick">
|
{/*<div className="actions quick">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="Mute"
|
content="Mute"
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
@@ -130,9 +130,9 @@ export function Titlebar(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>*/}
|
</div>*/}
|
||||||
<div class="drag" />
|
<div className="drag" />
|
||||||
<UpdateIndicator style="titlebar" />
|
<UpdateIndicator style="titlebar" />
|
||||||
<div class="actions">
|
<div className="actions">
|
||||||
<div onClick={window.native.min}>
|
<div onClick={window.native.min}>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="false"
|
aria-hidden="false"
|
||||||
@@ -164,7 +164,7 @@ export function Titlebar(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={window.native.close} class="error">
|
<div onClick={window.native.close} className="error">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="false"
|
aria-hidden="false"
|
||||||
width="12"
|
width="12"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useHistory, useLocation } from "react-router";
|
import { useHistory, useLocation } from "react-router";
|
||||||
import styled, { css } from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
|
import { Centred, IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import ConditionalLink from "../../lib/ConditionalLink";
|
import ConditionalLink from "../../lib/ConditionalLink";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
@@ -10,7 +12,6 @@ import { useApplicationState } from "../../mobx/State";
|
|||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import UserIcon from "../common/user/UserIcon";
|
import UserIcon from "../common/user/UserIcon";
|
||||||
import IconButton from "../ui/IconButton";
|
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
background: var(--secondary-background);
|
background: var(--secondary-background);
|
||||||
@@ -24,8 +25,19 @@ const Navbar = styled.div`
|
|||||||
height: var(--bottom-navigation-height);
|
height: var(--bottom-navigation-height);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I've decided that this whole component
|
||||||
|
* needs to be re-written 👍👍👍👍👍👍
|
||||||
|
*/
|
||||||
|
|
||||||
const Button = styled.a<{ active: boolean }>`
|
const Button = styled.a<{ active: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
// ok
|
||||||
|
* {
|
||||||
|
color: var(--foreground) !important;
|
||||||
|
}
|
||||||
|
|
||||||
> a,
|
> a,
|
||||||
> div,
|
> div,
|
||||||
@@ -63,7 +75,7 @@ export default observer(() => {
|
|||||||
<Base>
|
<Base>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Button active={homeActive}>
|
<Button active={homeActive}>
|
||||||
<IconButton
|
<Centred
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (settingsActive) {
|
if (settingsActive) {
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
@@ -80,14 +92,14 @@ export default observer(() => {
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Message size={24} />
|
<Message size={24} />
|
||||||
</IconButton>
|
</Centred>
|
||||||
</Button>
|
</Button>
|
||||||
<Button active={friendsActive}>
|
<Button active={friendsActive}>
|
||||||
<ConditionalLink active={friendsActive} to="/friends">
|
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
<ConditionalLink active={friendsActive} to="/friends">
|
||||||
<Group size={25} />
|
<Group size={25} />
|
||||||
</IconButton>
|
|
||||||
</ConditionalLink>
|
</ConditionalLink>
|
||||||
|
</IconButton>
|
||||||
</Button>
|
</Button>
|
||||||
{/*<Button active={searchActive}>
|
{/*<Button active={searchActive}>
|
||||||
<ConditionalLink active={searchActive} to="/search">
|
<ConditionalLink active={searchActive} to="/search">
|
||||||
@@ -104,20 +116,20 @@ export default observer(() => {
|
|||||||
</ConditionalLink>
|
</ConditionalLink>
|
||||||
</Button>*/}
|
</Button>*/}
|
||||||
<Button active={discoverActive}>
|
<Button active={discoverActive}>
|
||||||
|
<IconButton>
|
||||||
<ConditionalLink
|
<ConditionalLink
|
||||||
active={discoverActive}
|
active={discoverActive}
|
||||||
to="/discover/servers">
|
to="/discover/servers">
|
||||||
<IconButton>
|
|
||||||
<Compass size={24} />
|
<Compass size={24} />
|
||||||
</IconButton>
|
|
||||||
</ConditionalLink>
|
</ConditionalLink>
|
||||||
|
</IconButton>
|
||||||
</Button>
|
</Button>
|
||||||
<Button active={settingsActive}>
|
<Button active={settingsActive}>
|
||||||
<ConditionalLink active={settingsActive} to="/settings">
|
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
<ConditionalLink active={settingsActive} to="/settings">
|
||||||
<UserIcon target={user} size={26} status={true} />
|
<UserIcon target={user} size={26} status={true} />
|
||||||
</IconButton>
|
|
||||||
</ConditionalLink>
|
</ConditionalLink>
|
||||||
|
</IconButton>
|
||||||
</Button>
|
</Button>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</Base>
|
</Base>
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { User, Channel } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./Item.module.scss";
|
import styles from "./Item.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Ref } from "preact";
|
|
||||||
import { useTriggerEvents } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
import { Localizer, Text } from "preact-i18n";
|
import { Localizer, Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||||
|
|
||||||
@@ -19,9 +20,6 @@ import Tooltip from "../../common/Tooltip";
|
|||||||
import UserIcon from "../../common/user/UserIcon";
|
import UserIcon from "../../common/user/UserIcon";
|
||||||
import { Username } from "../../common/user/UserShort";
|
import { Username } from "../../common/user/UserShort";
|
||||||
import UserStatus from "../../common/user/UserStatus";
|
import UserStatus from "../../common/user/UserStatus";
|
||||||
import IconButton from "../../ui/IconButton";
|
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
|
|
||||||
type CommonProps = Omit<
|
type CommonProps = Omit<
|
||||||
JSX.HTMLAttributes<HTMLDivElement>,
|
JSX.HTMLAttributes<HTMLDivElement>,
|
||||||
@@ -166,11 +164,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||||||
channel: channel._id,
|
channel: channel._id,
|
||||||
unread: !!alert,
|
unread: !!alert,
|
||||||
})}>
|
})}>
|
||||||
<ChannelIcon
|
<div className={styles.avatar}>
|
||||||
className={styles.avatar}
|
<ChannelIcon target={channel} size={compact ? 24 : 32} />
|
||||||
target={channel}
|
</div>
|
||||||
size={compact ? 24 : 32}
|
|
||||||
/>
|
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
<div>{channel.name}</div>
|
<div>{channel.name}</div>
|
||||||
{channel.channel_type === "Group" && (
|
{channel.channel_type === "Group" && (
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Banner } from "@revoltchat/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
useClient,
|
useClient,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Banner from "../../ui/Banner";
|
|
||||||
|
|
||||||
export default function ConnectionStatus() {
|
export default function ConnectionStatus() {
|
||||||
const status = useContext(StatusContext);
|
const status = useContext(StatusContext);
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Plus } from "@styled-icons/boxicons-regular";
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
UserDetail,
|
UserDetail,
|
||||||
@@ -11,6 +12,8 @@ import styled, { css } from "styled-components/macro";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
import { useContext, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category, IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||||
import PaintCounter from "../../../lib/PaintCounter";
|
import PaintCounter from "../../../lib/PaintCounter";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
@@ -20,7 +23,6 @@ import { useApplicationState } from "../../../mobx/State";
|
|||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Category from "../../ui/Category";
|
|
||||||
import placeholderSVG from "../items/placeholder.svg";
|
import placeholderSVG from "../items/placeholder.svg";
|
||||||
|
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
@@ -125,15 +127,18 @@ export default observer(() => {
|
|||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Category
|
<Category>
|
||||||
text={<Text id="app.main.categories.conversations" />}
|
<Text id="app.main.categories.conversations" />
|
||||||
action={() =>
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
openScreen({
|
openScreen({
|
||||||
id: "special_input",
|
id: "special_input",
|
||||||
type: "create_group",
|
type: "create_group",
|
||||||
})
|
})
|
||||||
}
|
}>
|
||||||
/>
|
<Plus size={16} />
|
||||||
|
</IconButton>
|
||||||
|
</Category>
|
||||||
{channels.length === 0 && (
|
{channels.length === 0 && (
|
||||||
<img src={placeholderSVG} loading="eager" />
|
<img src={placeholderSVG} loading="eager" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Redirect, useParams } from "react-router";
|
import { Redirect, useParams } from "react-router";
|
||||||
import { Server } from "revolt.js";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { Ref } from "preact";
|
|
||||||
import { useTriggerEvents } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category } from "@revoltchat/ui";
|
||||||
|
|
||||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||||
import PaintCounter from "../../../lib/PaintCounter";
|
import PaintCounter from "../../../lib/PaintCounter";
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
@@ -18,8 +18,6 @@ import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|||||||
|
|
||||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||||
import ServerHeader from "../../common/ServerHeader";
|
import ServerHeader from "../../common/ServerHeader";
|
||||||
import Category from "../../ui/Category";
|
|
||||||
|
|
||||||
import { ChannelButton } from "../items/ButtonItem";
|
import { ChannelButton } from "../items/ButtonItem";
|
||||||
import ConnectionStatus from "../items/ConnectionStatus";
|
import ConnectionStatus from "../items/ConnectionStatus";
|
||||||
|
|
||||||
@@ -126,7 +124,7 @@ export default observer(() => {
|
|||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
id={`category_${category.id}`}
|
id={`category_${category.id}`}
|
||||||
defaultValue
|
defaultValue
|
||||||
summary={<Category text={category.title} />}>
|
summary={<Category>{category.title}</Category>}>
|
||||||
{channels}
|
{channels}
|
||||||
</CollapsibleSection>,
|
</CollapsibleSection>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export default function MemberList({
|
|||||||
server, see issue{" "}
|
server, see issue{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/revoltchat/delta/issues/128"
|
href="https://github.com/revoltchat/delta/issues/128"
|
||||||
target="_blank">
|
target="_blank" rel="noreferrer">
|
||||||
#128
|
#128
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
for when this will be resolved.
|
for when this will be resolved.
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const GroupMemberSidebar = observer(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ! FIXME: this is temporary code until we get lazy guilds like subscriptions
|
// ! FIXME: this is temporary code until we get lazy guilds like subscriptions
|
||||||
const FETCHED: Set<String> = new Set();
|
const FETCHED: Set<string> = new Set();
|
||||||
|
|
||||||
export function resetMemberSidebarFetched() {
|
export function resetMemberSidebarFetched() {
|
||||||
FETCHED.clear();
|
FETCHED.clear();
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import styled from "styled-components/macro";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Message from "../../common/messaging/Message";
|
import Message from "../../common/messaging/Message";
|
||||||
import InputBox from "../../ui/InputBox";
|
|
||||||
import Overline from "../../ui/Overline";
|
|
||||||
import Preloader from "../../ui/Preloader";
|
|
||||||
|
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
|
|
||||||
type SearchState =
|
type SearchState =
|
||||||
@@ -103,18 +99,20 @@ export function SearchSidebar({ close }: Props) {
|
|||||||
<GenericSidebarBase data-scroll-offset="with-padding">
|
<GenericSidebarBase data-scroll-offset="with-padding">
|
||||||
<GenericSidebarList>
|
<GenericSidebarList>
|
||||||
<SearchBase>
|
<SearchBase>
|
||||||
<Overline type="accent" block hover>
|
<Category>
|
||||||
<a onClick={close}>« back to members</a>
|
<Error
|
||||||
</Overline>
|
error={<a onClick={close}>« back to members</a>}
|
||||||
<Overline type="subtle" block>
|
/>
|
||||||
|
</Category>
|
||||||
|
<Category>
|
||||||
<Text id="app.main.channel.search.title" />
|
<Text id="app.main.channel.search.title" />
|
||||||
</Overline>
|
</Category>
|
||||||
<InputBox
|
<InputBox
|
||||||
value={query}
|
value={query}
|
||||||
onKeyDown={(e) => e.key === "Enter" && search()}
|
onKeyDown={(e) => e.key === "Enter" && search()}
|
||||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<div class="sort">
|
<div className="sort">
|
||||||
{["Latest", "Oldest", "Relevance"].map((key) => (
|
{["Latest", "Oldest", "Relevance"].map((key) => (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
@@ -129,7 +127,7 @@ export function SearchSidebar({ close }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{state.type === "loading" && <Preloader type="ring" />}
|
{state.type === "loading" && <Preloader type="ring" />}
|
||||||
{state.type === "results" && (
|
{state.type === "results" && (
|
||||||
<div class="list">
|
<div className="list">
|
||||||
{state.results.map((message) => {
|
{state.results.map((message) => {
|
||||||
let href = "";
|
let href = "";
|
||||||
if (channel?.channel_type === "TextChannel") {
|
if (channel?.channel_type === "TextChannel") {
|
||||||
@@ -140,7 +138,7 @@ export function SearchSidebar({ close }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={href} key={message._id}>
|
<Link to={href} key={message._id}>
|
||||||
<div class="message">
|
<div className="message">
|
||||||
<Message
|
<Message
|
||||||
message={message}
|
message={message}
|
||||||
head
|
head
|
||||||
|
|||||||
62
src/components/settings/account/AccountManagement.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Block } from "@styled-icons/boxicons-regular";
|
||||||
|
import { Trash } from "@styled-icons/boxicons-solid";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
import { CategoryButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../../context/modals";
|
||||||
|
import {
|
||||||
|
LogOutContext,
|
||||||
|
useClient,
|
||||||
|
} from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
export default function AccountManagement() {
|
||||||
|
const logOut = useContext(LogOutContext);
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const callback = (route: "disable" | "delete") => () =>
|
||||||
|
modalController.mfaFlow(client).then(
|
||||||
|
(ticket) =>
|
||||||
|
ticket &&
|
||||||
|
client.api
|
||||||
|
.post(`/auth/account/${route}`, undefined, {
|
||||||
|
headers: {
|
||||||
|
"X-MFA-Ticket": ticket.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => logOut(true)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.account.manage.title" />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<h5>
|
||||||
|
<Text id="app.settings.pages.account.manage.description" />
|
||||||
|
</h5>
|
||||||
|
<CategoryButton
|
||||||
|
icon={<Block size={24} color="var(--error)" />}
|
||||||
|
description={
|
||||||
|
"Disable your account. You won't be able to access it unless you contact support."
|
||||||
|
}
|
||||||
|
action="chevron"
|
||||||
|
onClick={callback("disable")}>
|
||||||
|
<Text id="app.settings.pages.account.manage.disable" />
|
||||||
|
</CategoryButton>
|
||||||
|
|
||||||
|
<CategoryButton
|
||||||
|
icon={<Trash size={24} color="var(--error)" />}
|
||||||
|
description={
|
||||||
|
"Your account will be queued for deletion, a confirmation email will be sent."
|
||||||
|
}
|
||||||
|
action="chevron"
|
||||||
|
onClick={callback("delete")}>
|
||||||
|
<Text id="app.settings.pages.account.manage.delete" />
|
||||||
|
</CategoryButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
src/components/settings/account/EditAccount.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { At } from "@styled-icons/boxicons-regular";
|
||||||
|
import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccountDetail,
|
||||||
|
CategoryButton,
|
||||||
|
Column,
|
||||||
|
HiddenValue,
|
||||||
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../../context/modals";
|
||||||
|
import {
|
||||||
|
ClientStatus,
|
||||||
|
StatusContext,
|
||||||
|
useClient,
|
||||||
|
} from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
export default observer(() => {
|
||||||
|
const client = useClient();
|
||||||
|
const status = useContext(StatusContext);
|
||||||
|
|
||||||
|
const [email, setEmail] = useState("...");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||||
|
client.api
|
||||||
|
.get("/auth/account/")
|
||||||
|
.then((account) => setEmail(account.email));
|
||||||
|
}
|
||||||
|
}, [client, email, status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Column group>
|
||||||
|
<AccountDetail user={client.user!} />
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["username", client.user!.username, At],
|
||||||
|
["email", email, Envelope],
|
||||||
|
["password", "•••••••••", Key],
|
||||||
|
] as const
|
||||||
|
).map(([field, value, Icon]) => (
|
||||||
|
<CategoryButton
|
||||||
|
key={field}
|
||||||
|
icon={<Icon size={24} />}
|
||||||
|
description={
|
||||||
|
field === "email" ? (
|
||||||
|
<HiddenValue
|
||||||
|
value={value}
|
||||||
|
placeholder={"•••••••••••@••••••.•••"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
account
|
||||||
|
action={<Pencil size={20} />}
|
||||||
|
onClick={() =>
|
||||||
|
modalController.push({
|
||||||
|
type: "modify_account",
|
||||||
|
client,
|
||||||
|
field,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Text id={`login.${field}`} />
|
||||||
|
</CategoryButton>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
222
src/components/settings/account/MultiFactorAuthentication.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { ListOl } from "@styled-icons/boxicons-regular";
|
||||||
|
import { Lock } from "@styled-icons/boxicons-solid";
|
||||||
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../../context/modals";
|
||||||
|
import {
|
||||||
|
ClientStatus,
|
||||||
|
StatusContext,
|
||||||
|
useClient,
|
||||||
|
} from "../../../context/revoltjs/RevoltClient";
|
||||||
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary helper function for Axios config
|
||||||
|
* @param token Token
|
||||||
|
* @returns Headers
|
||||||
|
*/
|
||||||
|
export function toConfig(token: string) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
"X-MFA-Ticket": token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for configuring MFA on an account.
|
||||||
|
*/
|
||||||
|
export default function MultiFactorAuthentication() {
|
||||||
|
// Pull in prerequisites
|
||||||
|
const client = useClient();
|
||||||
|
const status = useContext(StatusContext);
|
||||||
|
|
||||||
|
// Keep track of MFA state
|
||||||
|
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
|
// Fetch the current MFA status on account
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mfa && status === ClientStatus.ONLINE) {
|
||||||
|
client.api
|
||||||
|
.get("/auth/mfa/")
|
||||||
|
.then(setMFA)
|
||||||
|
.catch((err) => setError(takeError(err)));
|
||||||
|
}
|
||||||
|
}, [client, mfa, status]);
|
||||||
|
|
||||||
|
// Action called when recovery code button is pressed
|
||||||
|
const recoveryAction = useCallback(async () => {
|
||||||
|
// Perform MFA flow first
|
||||||
|
const ticket = await modalController.mfaFlow(client);
|
||||||
|
|
||||||
|
// Check whether action was cancelled
|
||||||
|
if (typeof ticket === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide whether to generate or fetch.
|
||||||
|
let codes;
|
||||||
|
if (mfa!.recovery_active) {
|
||||||
|
// Fetch existing recovery codes
|
||||||
|
codes = await client.api.post(
|
||||||
|
"/auth/mfa/recovery",
|
||||||
|
undefined,
|
||||||
|
toConfig(ticket.token),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Generate new recovery codes
|
||||||
|
codes = await client.api.patch(
|
||||||
|
"/auth/mfa/recovery",
|
||||||
|
undefined,
|
||||||
|
toConfig(ticket.token),
|
||||||
|
);
|
||||||
|
|
||||||
|
setMFA({
|
||||||
|
...mfa!,
|
||||||
|
recovery_active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the codes to the user
|
||||||
|
modalController.push({
|
||||||
|
type: "mfa_recovery",
|
||||||
|
client,
|
||||||
|
codes,
|
||||||
|
});
|
||||||
|
}, [mfa]);
|
||||||
|
|
||||||
|
// Action called when TOTP button is pressed
|
||||||
|
const totpAction = useCallback(async () => {
|
||||||
|
// Perform MFA flow first
|
||||||
|
const ticket = await modalController.mfaFlow(client);
|
||||||
|
|
||||||
|
// Check whether action was cancelled
|
||||||
|
if (typeof ticket === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide whether to disable or enable.
|
||||||
|
if (mfa!.totp_mfa) {
|
||||||
|
// Disable TOTP authentication
|
||||||
|
await client.api.delete("/auth/mfa/totp", toConfig(ticket.token));
|
||||||
|
|
||||||
|
setMFA({
|
||||||
|
...mfa!,
|
||||||
|
totp_mfa: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Generate a TOTP secret
|
||||||
|
const { secret } = await client.api.post(
|
||||||
|
"/auth/mfa/totp",
|
||||||
|
undefined,
|
||||||
|
toConfig(ticket.token),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open secret modal
|
||||||
|
let success;
|
||||||
|
while (!success) {
|
||||||
|
try {
|
||||||
|
// Make the user generator a token
|
||||||
|
const totp_code = await modalController.mfaEnableTOTP(
|
||||||
|
secret,
|
||||||
|
client.user!.username,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (totp_code) {
|
||||||
|
// Check whether it is valid
|
||||||
|
await client.api.put(
|
||||||
|
"/auth/mfa/totp",
|
||||||
|
{
|
||||||
|
totp_code,
|
||||||
|
},
|
||||||
|
toConfig(ticket.token),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark as successful and activated
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
setMFA({
|
||||||
|
...mfa!,
|
||||||
|
totp_mfa: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mfa]);
|
||||||
|
|
||||||
|
const mfaActive = !!mfa?.totp_mfa;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.account.2fa.title" />
|
||||||
|
</h3>
|
||||||
|
<h5>
|
||||||
|
<Text id="app.settings.pages.account.2fa.description" />
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Category compact>
|
||||||
|
<Error error={error} />
|
||||||
|
</Category>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CategoryButton
|
||||||
|
icon={<ListOl size={24} />}
|
||||||
|
description={
|
||||||
|
<Text
|
||||||
|
id={`app.settings.pages.account.2fa.${
|
||||||
|
mfa?.recovery_active
|
||||||
|
? "view_recovery"
|
||||||
|
: "generate_recovery"
|
||||||
|
}_long`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={!mfa}
|
||||||
|
onClick={recoveryAction}>
|
||||||
|
<Text
|
||||||
|
id={`app.settings.pages.account.2fa.${
|
||||||
|
mfa?.recovery_active
|
||||||
|
? "view_recovery"
|
||||||
|
: "generate_recovery"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</CategoryButton>
|
||||||
|
<CategoryButton
|
||||||
|
icon={
|
||||||
|
<Lock
|
||||||
|
size={24}
|
||||||
|
color={!mfa?.totp_mfa ? "var(--error)" : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description={"Set up time-based one-time password."}
|
||||||
|
disabled={!mfa || (!mfa.recovery_active && !mfa.totp_mfa)}
|
||||||
|
onClick={totpAction}>
|
||||||
|
<Text
|
||||||
|
id={`app.settings.pages.account.2fa.${
|
||||||
|
mfa?.totp_mfa ? "remove" : "add"
|
||||||
|
}_auth`}
|
||||||
|
/>
|
||||||
|
</CategoryButton>
|
||||||
|
|
||||||
|
{mfa && (
|
||||||
|
<Tip palette={mfaActive ? "primary" : "error"}>
|
||||||
|
<Text
|
||||||
|
id={`app.settings.pages.account.2fa.two_factor_${
|
||||||
|
mfaActive ? "on" : "off"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Tip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ import styled from "styled-components/macro";
|
|||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import mutantSVG from "./assets/mutant_emoji.svg";
|
||||||
|
import notoSVG from "./assets/noto_emoji.svg";
|
||||||
|
import openmojiSVG from "./assets/openmoji_emoji.svg";
|
||||||
|
import twemojiSVG from "./assets/twemoji_emoji.svg";
|
||||||
|
|
||||||
import { EmojiPack } from "../../common/Emoji";
|
import { EmojiPack } from "../../common/Emoji";
|
||||||
import mutantSVG from "./mutant_emoji.svg";
|
|
||||||
import notoSVG from "./noto_emoji.svg";
|
|
||||||
import openmojiSVG from "./openmoji_emoji.svg";
|
|
||||||
import twemojiSVG from "./twemoji_emoji.svg";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -87,10 +88,10 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||||
</h3>
|
</h3>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="row">
|
<div className="row">
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="button"
|
className="button"
|
||||||
onClick={() => setValue("mutant")}
|
onClick={() => setValue("mutant")}
|
||||||
data-active={!value || value === "mutant"}>
|
data-active={!value || value === "mutant"}>
|
||||||
<img
|
<img
|
||||||
@@ -112,7 +113,7 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="button"
|
className="button"
|
||||||
onClick={() => setValue("twemoji")}
|
onClick={() => setValue("twemoji")}
|
||||||
data-active={value === "twemoji"}>
|
data-active={value === "twemoji"}>
|
||||||
<img
|
<img
|
||||||
@@ -125,10 +126,10 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||||||
<h4>Twemoji</h4>
|
<h4>Twemoji</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div className="row">
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="button"
|
className="button"
|
||||||
onClick={() => setValue("openmoji")}
|
onClick={() => setValue("openmoji")}
|
||||||
data-active={value === "openmoji"}>
|
data-active={value === "openmoji"}>
|
||||||
<img
|
<img
|
||||||
@@ -142,7 +143,7 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="button"
|
className="button"
|
||||||
onClick={() => setValue("noto")}
|
onClick={() => setValue("noto")}
|
||||||
data-active={value === "noto"}>
|
data-active={value === "noto"}>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ import pSBC from "shade-blend-color";
|
|||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
|
import {
|
||||||
|
CategoryButton,
|
||||||
|
Checkbox,
|
||||||
|
ColourSwatches,
|
||||||
|
ComboBox,
|
||||||
|
Radio,
|
||||||
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Fonts,
|
Fonts,
|
||||||
@@ -17,21 +25,15 @@ import {
|
|||||||
MonospaceFonts,
|
MonospaceFonts,
|
||||||
MONOSPACE_FONTS,
|
MONOSPACE_FONTS,
|
||||||
MONOSPACE_FONT_KEYS,
|
MONOSPACE_FONT_KEYS,
|
||||||
} from "../../context/Theme";
|
} from "../../../context/Theme";
|
||||||
|
|
||||||
import Checkbox from "../ui/Checkbox";
|
import { EmojiSelector } from "./EmojiSelector";
|
||||||
import ColourSwatches from "../ui/ColourSwatches";
|
import { ThemeBaseSelector } from "./ThemeBaseSelector";
|
||||||
import ComboBox from "../ui/ComboBox";
|
|
||||||
import Radio from "../ui/Radio";
|
|
||||||
import CategoryButton from "../ui/fluent/CategoryButton";
|
|
||||||
|
|
||||||
import { EmojiSelector } from "./appearance/EmojiSelector";
|
|
||||||
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component providing a way to switch the base theme being used.
|
* Component providing a way to switch the base theme being used.
|
||||||
*/
|
*/
|
||||||
export const ThemeBaseSelectorShim = observer(() => {
|
export const ShimThemeBaseSelector = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
<ThemeBaseSelector
|
<ThemeBaseSelector
|
||||||
@@ -47,9 +49,8 @@ export const ThemeBaseSelectorShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a link to the theme shop.
|
* Component providing a link to the theme shop.
|
||||||
* Only appears if experiment is enabled.
|
* Only appears if experiment is enabled.
|
||||||
* TODO: stabilise
|
|
||||||
*/
|
*/
|
||||||
export const ThemeShopShim = () => {
|
export const ShimThemeShop = () => {
|
||||||
return (
|
return (
|
||||||
<Link to="/discover/themes" replace>
|
<Link to="/discover/themes" replace>
|
||||||
<CategoryButton
|
<CategoryButton
|
||||||
@@ -57,8 +58,7 @@ export const ThemeShopShim = () => {
|
|||||||
action="chevron"
|
action="chevron"
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.discover.description" />
|
<Text id="app.settings.pages.appearance.discover.description" />
|
||||||
}
|
}>
|
||||||
hover>
|
|
||||||
<Text id="app.settings.pages.appearance.discover.title" />
|
<Text id="app.settings.pages.appearance.discover.title" />
|
||||||
</CategoryButton>
|
</CategoryButton>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -68,7 +68,7 @@ export const ThemeShopShim = () => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to change current accent colour.
|
* Component providing a way to change current accent colour.
|
||||||
*/
|
*/
|
||||||
export const ThemeAccentShim = observer(() => {
|
export const ShimThemeAccent = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -89,7 +89,7 @@ export const ThemeAccentShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to edit custom CSS.
|
* Component providing a way to edit custom CSS.
|
||||||
*/
|
*/
|
||||||
export const ThemeCustomCSSShim = observer(() => {
|
export const ShimThemeCustomCSS = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -110,7 +110,7 @@ export const ThemeCustomCSSShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to switch between compact and normal message view.
|
* Component providing a way to switch between compact and normal message view.
|
||||||
*/
|
*/
|
||||||
export const DisplayCompactShim = () => {
|
export const ShimDisplayCompact = () => {
|
||||||
// TODO: WIP feature
|
// TODO: WIP feature
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -119,19 +119,24 @@ export const DisplayCompactShim = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div /* className={styles.display} */>
|
<div /* className={styles.display} */>
|
||||||
<Radio
|
<Radio
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.display.default" />
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
<Text id="app.settings.pages.appearance.display.default_description" />
|
||||||
}
|
}
|
||||||
checked>
|
value={true}
|
||||||
<Text id="app.settings.pages.appearance.display.default" />
|
/>
|
||||||
</Radio>
|
|
||||||
<Radio
|
<Radio
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.display.compact" />
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
<Text id="app.settings.pages.appearance.display.compact_description" />
|
||||||
}
|
}
|
||||||
disabled>
|
value={false}
|
||||||
<Text id="app.settings.pages.appearance.display.compact" />
|
disabled
|
||||||
</Radio>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -140,7 +145,7 @@ export const DisplayCompactShim = () => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to change primary text font.
|
* Component providing a way to change primary text font.
|
||||||
*/
|
*/
|
||||||
export const DisplayFontShim = observer(() => {
|
export const ShimDisplayFont = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -163,7 +168,7 @@ export const DisplayFontShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to change secondary, monospace text font.
|
* Component providing a way to change secondary, monospace text font.
|
||||||
*/
|
*/
|
||||||
export const DisplayMonospaceFontShim = observer(() => {
|
export const ShimDisplayMonospaceFont = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -193,20 +198,20 @@ export const DisplayMonospaceFontShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to toggle font ligatures.
|
* Component providing a way to toggle font ligatures.
|
||||||
*/
|
*/
|
||||||
export const DisplayLigaturesShim = observer(() => {
|
export const ShimDisplayLigatures = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
if (settings.theme.getFont() !== "Inter") return null;
|
if (settings.theme.getFont() !== "Inter") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.get("appearance:ligatures") ?? false}
|
value={settings.get("appearance:ligatures") ?? false}
|
||||||
onChange={(v) => settings.set("appearance:ligatures", v)}
|
onChange={(v) => settings.set("appearance:ligatures", v)}
|
||||||
|
title={<Text id="app.settings.pages.appearance.ligatures" />}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||||
}>
|
}
|
||||||
<Text id="app.settings.pages.appearance.ligatures" />
|
/>
|
||||||
</Checkbox>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -214,61 +219,67 @@ export const DisplayLigaturesShim = observer(() => {
|
|||||||
/**
|
/**
|
||||||
* Component providing a way to toggle showing the send button on desktop.
|
* Component providing a way to toggle showing the send button on desktop.
|
||||||
*/
|
*/
|
||||||
export const ShowSendButtonShim = observer(() => {
|
export const ShimShowSendButton = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.get("appearance:show_send_button") ?? false}
|
value={settings.get("appearance:show_send_button") ?? false}
|
||||||
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
||||||
}>
|
}
|
||||||
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
/>
|
||||||
</Checkbox>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component providing a way to toggle seasonal themes.
|
* Component providing a way to toggle seasonal themes.
|
||||||
*/
|
*/
|
||||||
export const DisplaySeasonalShim = observer(() => {
|
export const ShimDisplaySeasonal = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.get("appearance:seasonal") ?? true}
|
value={settings.get("appearance:seasonal") ?? true}
|
||||||
onChange={(v) => settings.set("appearance:seasonal", v)}
|
onChange={(v) => settings.set("appearance:seasonal", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
||||||
}>
|
}
|
||||||
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
/>
|
||||||
</Checkbox>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component providing a way to toggle transparency effects.
|
* Component providing a way to toggle transparency effects.
|
||||||
*/
|
*/
|
||||||
export const DisplayTransparencyShim = observer(() => {
|
export const ShimDisplayTransparency = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.get("appearance:transparency") ?? true}
|
value={settings.get("appearance:transparency") ?? true}
|
||||||
onChange={(v) => settings.set("appearance:transparency", v)}
|
onChange={(v) => settings.set("appearance:transparency", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
||||||
}>
|
}
|
||||||
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
/>
|
||||||
</Checkbox>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component providing a way to change emoji pack.
|
* Component providing a way to change emoji pack.
|
||||||
*/
|
*/
|
||||||
export const DisplayEmojiShim = observer(() => {
|
export const ShimDisplayEmoji = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
return (
|
return (
|
||||||
<EmojiSelector
|
<EmojiSelector
|
||||||
@@ -2,8 +2,8 @@ import styled from "styled-components/macro";
|
|||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import darkSVG from "./dark.svg";
|
import darkSVG from "./assets/dark.svg";
|
||||||
import lightSVG from "./light.svg";
|
import lightSVG from "./assets/light.svg";
|
||||||
|
|
||||||
const List = styled.div`
|
const List = styled.div`
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Pencil } from "@styled-icons/boxicons-regular";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
|
import { InputBox } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useDebounceCallback } from "../../../lib/debounce";
|
import { useDebounceCallback } from "../../../lib/debounce";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { Variables } from "../../../context/Theme";
|
import { Variables } from "../../../context/Theme";
|
||||||
|
|
||||||
import InputBox from "../../ui/InputBox";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -117,10 +117,10 @@ export default observer(() => {
|
|||||||
] as const
|
] as const
|
||||||
).map((key) => (
|
).map((key) => (
|
||||||
<div
|
<div
|
||||||
class="entry"
|
className="entry"
|
||||||
key={key}
|
key={key}
|
||||||
style={{ backgroundColor: theme.getVariable(key) }}>
|
style={{ backgroundColor: theme.getVariable(key) }}>
|
||||||
<div class="input">
|
<div className="input">
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={theme.getVariable(key)}
|
value={theme.getVariable(key)}
|
||||||
@@ -141,9 +141,9 @@ export default observer(() => {
|
|||||||
}}>
|
}}>
|
||||||
{key}
|
{key}
|
||||||
</span>
|
</span>
|
||||||
<div class="override">
|
<div className="override">
|
||||||
<div
|
<div
|
||||||
class="picker"
|
className="picker"
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
e.currentTarget.parentElement?.parentElement
|
e.currentTarget.parentElement?.parentElement
|
||||||
?.querySelector("input")
|
?.querySelector("input")
|
||||||
@@ -153,7 +153,7 @@ export default observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<InputBox
|
<InputBox
|
||||||
type="text"
|
type="text"
|
||||||
class="text"
|
className="text"
|
||||||
value={theme.getVariable(key)}
|
value={theme.getVariable(key)}
|
||||||
onChange={(el) =>
|
onChange={(el) =>
|
||||||
setVariable({
|
setVariable({
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function ThemeTools() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div
|
<div
|
||||||
class="code"
|
className="code"
|
||||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||||
<Tooltip content={<Text id="app.special.copy" />}>
|
<Tooltip content={<Text id="app.special.copy" />}>
|
||||||
{" "}
|
{" "}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -7,8 +7,7 @@ import styled, { css } from "styled-components";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useMemo } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
|
|
||||||
import Checkbox from "../../ui/Checkbox";
|
import { Checkbox, OverrideSwitch } from "@revoltchat/ui";
|
||||||
import { OverrideSwitch } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
interface PermissionSelectProps {
|
interface PermissionSelectProps {
|
||||||
id: keyof typeof Permission;
|
id: keyof typeof Permission;
|
||||||
@@ -69,13 +68,13 @@ export function PermissionSelect({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "Neutral";
|
return "Neutral";
|
||||||
} else {
|
}
|
||||||
if (Long.fromNumber(value).and(permission).eq(permission)) {
|
if (Long.fromNumber(value).and(permission).eq(permission)) {
|
||||||
return "Allow";
|
return "Allow";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Neutral";
|
return "Neutral";
|
||||||
}
|
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
function onSwitch(state: State) {
|
function onSwitch(state: State) {
|
||||||
@@ -118,12 +117,12 @@ export function PermissionSelect({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PermissionEntry disabled={disabled}>
|
<PermissionEntry disabled={disabled}>
|
||||||
<span class="title">
|
<span className="title">
|
||||||
<span>
|
<span>
|
||||||
<Text id={`permissions.${id}.t`}>{id}</Text>
|
<Text id={`permissions.${id}.t`}>{id}</Text>
|
||||||
{disabled && <Lock className="lock" size={14} />}
|
{disabled && <Lock className="lock" size={14} />}
|
||||||
</span>
|
</span>
|
||||||
<span class="description">
|
<span className="description">
|
||||||
<Text id={`permissions.${id}.d`} />
|
<Text id={`permissions.${id}.d`} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -136,7 +135,7 @@ export function PermissionSelect({
|
|||||||
) : (
|
) : (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={state === "Allow"}
|
value={state === "Allow"}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
onChange(
|
onChange(
|
||||||
Long.fromNumber(value, false)
|
Long.fromNumber(value, false)
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import styled from "styled-components/macro";
|
|
||||||
|
|
||||||
export default styled.div`
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
color: var(--accent);
|
|
||||||
background: var(--primary-background);
|
|
||||||
`;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Plus } from "@styled-icons/boxicons-regular";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
const CategoryBase = styled.div<Pick<Props, "variant">>`
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
margin-top: 4px;
|
|
||||||
padding: 6px 0 6px 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.variant === "uniform" &&
|
|
||||||
css`
|
|
||||||
padding-top: 6px;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = Omit<
|
|
||||||
JSX.HTMLAttributes<HTMLDivElement>,
|
|
||||||
"children" | "as" | "action"
|
|
||||||
> & {
|
|
||||||
text: Children;
|
|
||||||
action?: () => void;
|
|
||||||
variant?: "default" | "uniform";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Category(props: Props) {
|
|
||||||
const { text, action, ...otherProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CategoryBase {...otherProps}>
|
|
||||||
{text}
|
|
||||||
{action && <Plus size={16} onClick={action} />}
|
|
||||||
</CategoryBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { Check } from "@styled-icons/boxicons-regular";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
export const CheckboxBase = styled.label`
|
|
||||||
gap: 4px;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 20px;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 18px;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
transition: 0.2s ease all;
|
|
||||||
|
|
||||||
input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.check {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CheckboxContent = styled.span`
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 14px;
|
|
||||||
gap: 2px;
|
|
||||||
font-weight: 600;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CheckboxDescription = styled.span`
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Checkmark = styled.div<{ checked: boolean; contrast?: boolean }>`
|
|
||||||
margin: 4px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: grid;
|
|
||||||
flex-shrink: 0;
|
|
||||||
place-items: center;
|
|
||||||
transition: 0.2s ease all;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: var(--secondary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.contrast &&
|
|
||||||
css`
|
|
||||||
background: var(--primary-background);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: var(--primary-background);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.checked &&
|
|
||||||
css`
|
|
||||||
background: var(--accent) !important;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export interface CheckboxProps {
|
|
||||||
checked: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
contrast?: boolean;
|
|
||||||
className?: string;
|
|
||||||
children?: Children;
|
|
||||||
description?: Children;
|
|
||||||
onChange: (state: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Checkbox(props: CheckboxProps) {
|
|
||||||
return (
|
|
||||||
<CheckboxBase disabled={props.disabled} className={props.className}>
|
|
||||||
<CheckboxContent>
|
|
||||||
<span>{props.children}</span>
|
|
||||||
{props.description && (
|
|
||||||
<CheckboxDescription>
|
|
||||||
{props.description}
|
|
||||||
</CheckboxDescription>
|
|
||||||
)}
|
|
||||||
</CheckboxContent>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={props.checked}
|
|
||||||
onChange={() =>
|
|
||||||
!props.disabled && props.onChange(!props.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Checkmark
|
|
||||||
checked={props.checked}
|
|
||||||
contrast={props.contrast}
|
|
||||||
className="check">
|
|
||||||
<Check size={20} />
|
|
||||||
</Checkmark>
|
|
||||||
</CheckboxBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { Check } from "@styled-icons/boxicons-regular";
|
|
||||||
import { Palette } from "@styled-icons/boxicons-solid";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { RefObject } from "preact";
|
|
||||||
import { useRef } from "preact/hooks";
|
|
||||||
|
|
||||||
import { useDebounceCallback } from "../../lib/debounce";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presets = [
|
|
||||||
[
|
|
||||||
"#7B68EE",
|
|
||||||
"#3498DB",
|
|
||||||
"#1ABC9C",
|
|
||||||
"#F1C40F",
|
|
||||||
"#FF7F50",
|
|
||||||
"#FD6671",
|
|
||||||
"#E91E63",
|
|
||||||
"#D468EE",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"#594CAD",
|
|
||||||
"#206694",
|
|
||||||
"#11806A",
|
|
||||||
"#C27C0E",
|
|
||||||
"#CD5B45",
|
|
||||||
"#FF424F",
|
|
||||||
"#AD1457",
|
|
||||||
"#954AA8",
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const SwatchesBase = styled.div`
|
|
||||||
/*gap: 8px;*/
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
top: 72px;
|
|
||||||
opacity: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: relative;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
div {
|
|
||||||
width: 8px;
|
|
||||||
height: 68px;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--primary-background),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Swatch = styled.div<{ type: "small" | "large"; colour: string }>`
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: ${(props) => props.colour};
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 3px solid var(--foreground);
|
|
||||||
transition: border ease-in-out 0.07s;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.type === "small"
|
|
||||||
? css`
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
/*stroke-width: 2;*/
|
|
||||||
}
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
width: 68px;
|
|
||||||
height: 68px;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Rows = styled.div`
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding-inline-start: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ColourSwatches({ value, onChange }: Props) {
|
|
||||||
const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
|
|
||||||
const setValue = useDebounceCallback(
|
|
||||||
(value) => onChange(value as string),
|
|
||||||
[onChange],
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SwatchesBase>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={value}
|
|
||||||
ref={ref}
|
|
||||||
onChange={(ev) => setValue(ev.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<Swatch
|
|
||||||
colour={value}
|
|
||||||
type="large"
|
|
||||||
onClick={() => ref.current?.click()}>
|
|
||||||
<Palette size={32} />
|
|
||||||
</Swatch>
|
|
||||||
|
|
||||||
<div class="overlay">
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Rows>
|
|
||||||
{presets.map((row, i) => (
|
|
||||||
<div key={i}>
|
|
||||||
{row.map((swatch, i) => (
|
|
||||||
<Swatch
|
|
||||||
colour={swatch}
|
|
||||||
type="small"
|
|
||||||
key={i}
|
|
||||||
onClick={() => onChange(swatch)}>
|
|
||||||
{swatch === value && <Check size={22} />}
|
|
||||||
</Swatch>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Rows>
|
|
||||||
</SwatchesBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import styled from "styled-components/macro";
|
|
||||||
|
|
||||||
export default styled.select`
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: var(--text-size);
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
transition: box-shadow 0.2s ease-in-out;
|
|
||||||
transition: outline-color 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 1.5pt var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
`;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { dayjs } from "../../context/Locale";
|
|
||||||
|
|
||||||
const Base = styled.div<{ unread?: boolean }>`
|
|
||||||
height: 0;
|
|
||||||
display: flex;
|
|
||||||
user-select: none;
|
|
||||||
align-items: center;
|
|
||||||
margin: 17px 12px 5px;
|
|
||||||
border-top: thin solid var(--tertiary-foreground);
|
|
||||||
|
|
||||||
time {
|
|
||||||
margin-top: -2px;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
line-height: 0.6875rem;
|
|
||||||
padding-inline-start: 5px;
|
|
||||||
padding-inline-end: 5px;
|
|
||||||
color: var(--tertiary-foreground);
|
|
||||||
background: var(--primary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.unread &&
|
|
||||||
css`
|
|
||||||
border-top: thin solid var(--accent);
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Unread = styled.div`
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--accent-contrast);
|
|
||||||
font-size: 7px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 10px;
|
|
||||||
border-radius: 60px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: -1px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
date?: Date;
|
|
||||||
unread?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DateDivider({ unread, date }: Props) {
|
|
||||||
return (
|
|
||||||
<Base unread={unread}>
|
|
||||||
{unread && <Unread>NEW</Unread>}
|
|
||||||
{date && <time>{dayjs(date).format("LL")}</time>}
|
|
||||||
</Base>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
|
||||||
|
|
||||||
export default styled.details<{ sticky?: boolean; large?: boolean }>`
|
|
||||||
summary {
|
|
||||||
${(props) =>
|
|
||||||
props.sticky &&
|
|
||||||
css`
|
|
||||||
top: 48px;
|
|
||||||
z-index: 10;
|
|
||||||
position: sticky;
|
|
||||||
${() =>
|
|
||||||
isTouchscreenDevice &&
|
|
||||||
css`
|
|
||||||
top: 56px;
|
|
||||||
`}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.large &&
|
|
||||||
css`
|
|
||||||
/*padding: 5px 0;*/
|
|
||||||
background: var(--primary-background);
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
|
|
||||||
.padding {
|
|
||||||
/*TOFIX: make this applicable only for the friends list menu, DO NOT REMOVE.*/
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px 0;
|
|
||||||
margin: 0.8em 0px 0.4em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
user-select: none;
|
|
||||||
align-items: center;
|
|
||||||
transition: 0.2s opacity;
|
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
&::marker,
|
|
||||||
&::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-top: 1px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padding {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-inline-end: 4px;
|
|
||||||
transition: 0.2s ease transform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([open]) {
|
|
||||||
summary {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary svg {
|
|
||||||
transform: rotateZ(-90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -7,93 +7,22 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import styled, { css } from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
|
import { Header } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
|
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
topBorder?: boolean;
|
topBorder?: boolean;
|
||||||
bottomBorder?: boolean;
|
bottomBorder?: boolean;
|
||||||
|
|
||||||
background?: boolean;
|
withBackground?: boolean;
|
||||||
transparent?: boolean;
|
withTransparency?: boolean;
|
||||||
placement: "primary" | "secondary";
|
placement: "primary" | "secondary";
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = styled.div<Props>`
|
|
||||||
gap: 10px;
|
|
||||||
flex: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
height: var(--header-height);
|
|
||||||
|
|
||||||
background-size: cover !important;
|
|
||||||
background-position: center !important;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.transparent
|
|
||||||
? css`
|
|
||||||
background-color: rgba(
|
|
||||||
var(--primary-header-rgb),
|
|
||||||
max(var(--min-opacity), 0.75)
|
|
||||||
);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
z-index: 20;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
background-color: var(--primary-header);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.background &&
|
|
||||||
css`
|
|
||||||
height: 120px !important;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
text-shadow: 0px 0px 1px black;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.placement === "secondary" &&
|
|
||||||
css`
|
|
||||||
background-color: var(--secondary-header);
|
|
||||||
padding: 14px;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.topBorder &&
|
|
||||||
css`
|
|
||||||
border-start-start-radius: 8px;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.bottomBorder &&
|
|
||||||
css`
|
|
||||||
border-end-start-radius: 8px;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|
||||||
const IconContainer = styled.div`
|
const IconContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -128,7 +57,7 @@ export const PageHeader = observer(
|
|||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
{...props}
|
{...props}
|
||||||
placement="primary"
|
palette="primary"
|
||||||
topBorder={!visible}
|
topBorder={!visible}
|
||||||
bottomBorder={!pathname.includes("/server")}>
|
bottomBorder={!pathname.includes("/server")}>
|
||||||
{!noBurger && <HamburgerAction />}
|
{!noBurger && <HamburgerAction />}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
rotate?: string;
|
|
||||||
type?: "default" | "circle";
|
|
||||||
}
|
|
||||||
|
|
||||||
const normal = `var(--secondary-foreground)`;
|
|
||||||
const hover = `var(--foreground)`;
|
|
||||||
|
|
||||||
export default styled.div<Props>`
|
|
||||||
z-index: 1;
|
|
||||||
display: grid;
|
|
||||||
cursor: pointer;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
transition: 0.1s ease all;
|
|
||||||
|
|
||||||
fill: ${normal};
|
|
||||||
color: ${normal};
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: ${normal};
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
transition: 0.2s ease transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
fill: ${hover};
|
|
||||||
color: ${hover};
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: ${hover};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.type === "circle" &&
|
|
||||||
css`
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: var(--border-radius-half);
|
|
||||||
background-color: var(--secondary-header);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--primary-header);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.rotate &&
|
|
||||||
css`
|
|
||||||
svg {
|
|
||||||
transform: rotateZ(${props.rotate});
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
readonly contrast?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default styled.input<Props>`
|
|
||||||
z-index: 1;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
font-family: inherit;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--primary-background);
|
|
||||||
transition: 0.2s ease background-color;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
transition: outline-color 0.2s ease-in-out;
|
|
||||||
transition: box-shadow 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--secondary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 1.5pt var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.contrast &&
|
|
||||||
css`
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--hover);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import styled from "styled-components/macro";
|
|
||||||
|
|
||||||
export default styled.div`
|
|
||||||
height: 0;
|
|
||||||
opacity: 0.6;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin: 8px 15px;
|
|
||||||
border-top: 1px solid var(--tertiary-foreground);
|
|
||||||
`;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// This file must be imported and used at least once for SVG masks.
|
|
||||||
|
|
||||||
export default function Masks() {
|
|
||||||
return (
|
|
||||||
<svg width={0} height={0} style={{ position: "fixed" }}>
|
|
||||||
<defs>
|
|
||||||
<mask id="server">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<circle cx="27" cy="5" r="7" fill={"black"} />
|
|
||||||
</mask>
|
|
||||||
<mask id="user">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<circle cx="27" cy="27" r="7" fill={"black"} />
|
|
||||||
</mask>
|
|
||||||
<mask id="session">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<circle cx="26" cy="28" r="10" fill={"black"} />
|
|
||||||
</mask>
|
|
||||||
<mask id="overlap">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<circle cx="32" cy="16" r="18" fill={"black"} />
|
|
||||||
</mask>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import styled, { css, keyframes } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { createPortal, useCallback, useEffect, useState } from "preact/compat";
|
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
|
||||||
import { Props as ButtonProps } from "@revoltchat/ui/esm/components/design/atoms/inputs/Button";
|
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
const open = keyframes`
|
|
||||||
0% {opacity: 0;}
|
|
||||||
70% {opacity: 0;}
|
|
||||||
100% {opacity: 1;}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const close = keyframes`
|
|
||||||
0% {opacity: 1;}
|
|
||||||
70% {opacity: 0;}
|
|
||||||
100% {opacity: 0;}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const zoomIn = keyframes`
|
|
||||||
0% {transform: scale(0.5);}
|
|
||||||
98% {transform: scale(1.01);}
|
|
||||||
100% {transform: scale(1);}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const zoomOut = keyframes`
|
|
||||||
0% {transform: scale(1);}
|
|
||||||
100% {transform: scale(0.5);}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalBase = styled.div`
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 9999;
|
|
||||||
position: fixed;
|
|
||||||
max-height: 100%;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
animation-name: ${open};
|
|
||||||
animation-duration: 0.2s;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
overflow-y: auto;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
color: var(--foreground);
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
|
|
||||||
&.closing {
|
|
||||||
animation-name: ${close};
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.closing > div {
|
|
||||||
animation-name: ${zoomOut};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalContainer = styled.div`
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
animation-name: ${zoomIn};
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalContent = styled.div<
|
|
||||||
{ [key in "attachment" | "noBackground" | "border" | "padding"]?: boolean }
|
|
||||||
>`
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 14px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: var(--tertiary-foreground);
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
!props.noBackground &&
|
|
||||||
css`
|
|
||||||
background: var(--secondary-header);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.padding &&
|
|
||||||
css`
|
|
||||||
padding: 1rem;
|
|
||||||
min-width: 450px;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.attachment &&
|
|
||||||
css`
|
|
||||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.border &&
|
|
||||||
css`
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 2px solid var(--secondary-background);
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalActions = styled.div`
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--secondary-background);
|
|
||||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type Action = Omit<
|
|
||||||
JSX.HTMLAttributes<HTMLButtonElement>,
|
|
||||||
"as" | "onClick"
|
|
||||||
> & {
|
|
||||||
palette?: ButtonProps["palette"];
|
|
||||||
confirmation?: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: Children;
|
|
||||||
title?: Children;
|
|
||||||
description?: Children;
|
|
||||||
|
|
||||||
disallowClosing?: boolean;
|
|
||||||
noBackground?: boolean;
|
|
||||||
dontModal?: boolean;
|
|
||||||
padding?: boolean;
|
|
||||||
|
|
||||||
onClose?: () => void;
|
|
||||||
actions?: Action[];
|
|
||||||
disabled?: boolean;
|
|
||||||
palette?: ButtonProps["palette"];
|
|
||||||
border?: boolean;
|
|
||||||
visible: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let isModalClosing = false;
|
|
||||||
|
|
||||||
export default function Modal(props: Props) {
|
|
||||||
if (!props.visible) return null;
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<ModalContent
|
|
||||||
attachment={!!props.actions}
|
|
||||||
noBackground={props.noBackground}
|
|
||||||
border={props.border}
|
|
||||||
padding={props.padding ?? !props.dontModal}>
|
|
||||||
{props.title && <h3>{props.title}</h3>}
|
|
||||||
|
|
||||||
{props.description && <h5>{props.description}</h5>}
|
|
||||||
{props.children}
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.dontModal) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [animateClose, setAnimateClose] = useState(false);
|
|
||||||
isModalClosing = animateClose;
|
|
||||||
const onClose = useCallback(() => {
|
|
||||||
setAnimateClose(true);
|
|
||||||
setTimeout(() => props.onClose!(), 2e2);
|
|
||||||
}, [setAnimateClose, props]);
|
|
||||||
|
|
||||||
useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.disallowClosing) return;
|
|
||||||
|
|
||||||
function keyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
|
||||||
}, [props.disallowClosing, onClose]);
|
|
||||||
|
|
||||||
const confirmationAction = props.actions?.find(
|
|
||||||
(action) => action.confirmation,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!confirmationAction) return;
|
|
||||||
|
|
||||||
// ! TODO: this may be done better if we
|
|
||||||
// ! can focus the button although that
|
|
||||||
// ! doesn't seem to work...
|
|
||||||
function keyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
confirmationAction!.onClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
|
||||||
}, [confirmationAction]);
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<ModalBase
|
|
||||||
className={animateClose ? "closing" : undefined}
|
|
||||||
onClick={(!props.disallowClosing && props.onClose) || undefined}>
|
|
||||||
<ModalContainer onClick={(e) => (e.cancelBubble = true)}>
|
|
||||||
{content}
|
|
||||||
{props.actions && (
|
|
||||||
<ModalActions>
|
|
||||||
{props.actions.map((x, index) => (
|
|
||||||
<Button
|
|
||||||
palette={props.palette}
|
|
||||||
key={index}
|
|
||||||
{...x}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ModalActions>
|
|
||||||
)}
|
|
||||||
</ModalContainer>
|
|
||||||
</ModalBase>,
|
|
||||||
document.body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, "children" | "as"> & {
|
|
||||||
error?: string;
|
|
||||||
hover?: boolean;
|
|
||||||
block?: boolean;
|
|
||||||
spaced?: boolean;
|
|
||||||
noMargin?: boolean;
|
|
||||||
children?: Children;
|
|
||||||
type?: "default" | "subtle" | "error" | "accent";
|
|
||||||
};
|
|
||||||
|
|
||||||
const OverlineBase = styled.div<Omit<Props, "children" | "error">>`
|
|
||||||
display: inline;
|
|
||||||
transition: 0.2s ease filter;
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.hover &&
|
|
||||||
css`
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.2s ease filter;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(1.2);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
!props.noMargin &&
|
|
||||||
css`
|
|
||||||
margin: 0.4em 0;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.spaced &&
|
|
||||||
css`
|
|
||||||
margin-top: 0.8em;
|
|
||||||
`}
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--foreground);
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.type === "subtle" &&
|
|
||||||
css`
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.type === "error" &&
|
|
||||||
css`
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--error);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.type === "accent" &&
|
|
||||||
css`
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--accent);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.block &&
|
|
||||||
css`
|
|
||||||
display: block;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Overline(props: Props) {
|
|
||||||
return (
|
|
||||||
<OverlineBase {...props}>
|
|
||||||
{props.children}
|
|
||||||
{props.children && props.error && <> · </>}
|
|
||||||
{props.error && (
|
|
||||||
<Overline type="error">
|
|
||||||
<Text id={`error.${props.error}`}>{props.error}</Text>
|
|
||||||
</Overline>
|
|
||||||
)}
|
|
||||||
</OverlineBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import styled, { keyframes } from "styled-components/macro";
|
|
||||||
|
|
||||||
const skSpinner = keyframes`
|
|
||||||
0%, 80%, 100% {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
-webkit-transform: scale(1.0);
|
|
||||||
transform: scale(1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const prRing = keyframes`
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PreloaderBase = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 58px;
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
margin: 100px auto 0;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner > div {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background-color: var(--tertiary-foreground);
|
|
||||||
|
|
||||||
border-radius: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
animation: ${skSpinner} 1.4s infinite ease-in-out both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner div:nth-child(1) {
|
|
||||||
animation-delay: -0.32s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner div:nth-child(2) {
|
|
||||||
animation-delay: -0.16s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 48px;
|
|
||||||
height: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring div {
|
|
||||||
width: 32px;
|
|
||||||
margin: 8px;
|
|
||||||
height: 32px;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
border-radius: var(--border-radius-half);
|
|
||||||
animation: ${prRing} 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
|
||||||
border-color: #fff transparent transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring div:nth-child(1) {
|
|
||||||
animation-delay: -0.45s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring div:nth-child(2) {
|
|
||||||
animation-delay: -0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring div:nth-child(3) {
|
|
||||||
animation-delay: -0.15s;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
type: "spinner" | "ring";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Preloader({ type }: Props) {
|
|
||||||
return (
|
|
||||||
<PreloaderBase>
|
|
||||||
<div class={type}>
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
</PreloaderBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { Circle } from "@styled-icons/boxicons-regular";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: Children;
|
|
||||||
description?: Children;
|
|
||||||
|
|
||||||
checked?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
onSelect?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseProps {
|
|
||||||
selected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioBase = styled.label<BaseProps>`
|
|
||||||
gap: 4px;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
cursor: pointer;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
transition: 0.2s ease all;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
> input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin: 4px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: var(--foreground);
|
|
||||||
border-radius: var(--border-radius-half);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: var(--foreground);
|
|
||||||
/*stroke-width: 2;*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.selected &&
|
|
||||||
css`
|
|
||||||
color: white;
|
|
||||||
cursor: default;
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
> div {
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div svg {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RadioDescription = styled.span<BaseProps>`
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.selected &&
|
|
||||||
css`
|
|
||||||
color: white;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Radio(props: Props) {
|
|
||||||
const selected = props.checked ?? false;
|
|
||||||
return (
|
|
||||||
<RadioBase
|
|
||||||
selected={selected}
|
|
||||||
disabled={props.disabled}
|
|
||||||
onClick={() =>
|
|
||||||
!props.disabled && props.onSelect && props.onSelect()
|
|
||||||
}>
|
|
||||||
<div>
|
|
||||||
<Circle size={12} />
|
|
||||||
</div>
|
|
||||||
<input type="radio" checked={props.checked} />
|
|
||||||
<span>
|
|
||||||
<span>{props.children}</span>
|
|
||||||
{props.description && (
|
|
||||||
<RadioDescription selected={selected}>
|
|
||||||
{props.description}
|
|
||||||
</RadioDescription>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</RadioBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Check, CloudUpload } from "@styled-icons/boxicons-regular";
|
|
||||||
import { Pencil } from "@styled-icons/boxicons-solid";
|
|
||||||
import styled from "styled-components/macro";
|
|
||||||
|
|
||||||
const StatusBase = styled.div`
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-transform: capitalize;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type EditStatus = "saved" | "editing" | "saving";
|
|
||||||
interface Props {
|
|
||||||
status: EditStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SaveStatus({ status }: Props) {
|
|
||||||
return (
|
|
||||||
<StatusBase>
|
|
||||||
{status === "saved" ? (
|
|
||||||
<Check size={20} />
|
|
||||||
) : status === "editing" ? (
|
|
||||||
<Pencil size={20} />
|
|
||||||
) : (
|
|
||||||
<CloudUpload size={20} />
|
|
||||||
)}
|
|
||||||
{/* FIXME: add i18n */}
|
|
||||||
<span>{status}</span>
|
|
||||||
</StatusBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
.container {
|
|
||||||
font-size: .875rem;
|
|
||||||
line-height: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea {
|
|
||||||
width: 100%;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
textarea::placeholder {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost {
|
|
||||||
width: 100%;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
export interface TextAreaProps {
|
|
||||||
code?: boolean;
|
|
||||||
padding?: string;
|
|
||||||
lineHeight?: string;
|
|
||||||
hideBorder?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TEXT_AREA_BORDER_WIDTH = 2;
|
|
||||||
export const DEFAULT_TEXT_AREA_PADDING = 16;
|
|
||||||
export const DEFAULT_LINE_HEIGHT = 20;
|
|
||||||
|
|
||||||
export default styled.textarea<TextAreaProps>`
|
|
||||||
width: 100%;
|
|
||||||
resize: none;
|
|
||||||
display: block;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
padding: ${(props) => props.padding ?? "var(--textarea-padding)"};
|
|
||||||
line-height: ${(props) =>
|
|
||||||
props.lineHeight ?? "var(--textarea-line-height)"};
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.hideBorder &&
|
|
||||||
css`
|
|
||||||
border: none;
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
!props.hideBorder &&
|
|
||||||
css`
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: border-color 0.2s ease-in-out;
|
|
||||||
border: var(--input-border-width) solid transparent;
|
|
||||||
`}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
!props.hideBorder &&
|
|
||||||
css`
|
|
||||||
border: var(--input-border-width) solid var(--accent);
|
|
||||||
`}
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.code
|
|
||||||
? css`
|
|
||||||
font-family: var(--monospace-font), monospace;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
font-family: inherit;
|
|
||||||
`}
|
|
||||||
|
|
||||||
font-variant-ligatures: var(--ligatures);
|
|
||||||
`;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { InfoCircle } from "@styled-icons/boxicons-regular";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
warning?: boolean;
|
|
||||||
error?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Separator = styled.div<Props>`
|
|
||||||
height: 1px;
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
background: var(--secondary-header);
|
|
||||||
margin: 18px auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TipBase = styled.div<Props>`
|
|
||||||
display: flex;
|
|
||||||
padding: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
overflow: hidden;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
background: var(--primary-header);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 2px solid var(--secondary-header);
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-inline-end: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.warning &&
|
|
||||||
css`
|
|
||||||
color: var(--warning);
|
|
||||||
border: 2px solid var(--warning);
|
|
||||||
background: var(--secondary-header);
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.error &&
|
|
||||||
css`
|
|
||||||
color: white;
|
|
||||||
border: 2px solid var(--error);
|
|
||||||
background: var(--error);
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Tip(
|
|
||||||
props: Props & { children: Children; hideSeparator?: boolean },
|
|
||||||
) {
|
|
||||||
const { children, hideSeparator, ...tipProps } = props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!hideSeparator && <Separator />}
|
|
||||||
<TipBase {...tipProps}>
|
|
||||||
<InfoCircle size={20} />
|
|
||||||
<span>{children}</span>
|
|
||||||
</TipBase>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
ChevronRight,
|
|
||||||
LinkExternal,
|
|
||||||
Pencil,
|
|
||||||
} from "@styled-icons/boxicons-regular";
|
|
||||||
import styled, { css } from "styled-components/macro";
|
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
|
|
||||||
interface BaseProps {
|
|
||||||
readonly hover?: boolean;
|
|
||||||
readonly account?: boolean;
|
|
||||||
readonly disabled?: boolean;
|
|
||||||
readonly largeDescription?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CategoryBase = styled.div<BaseProps>`
|
|
||||||
padding: 9.8px 12px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--secondary-header);
|
|
||||||
gap: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
${(props) =>
|
|
||||||
props.largeDescription
|
|
||||||
? css`
|
|
||||||
font-size: 14px;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
font-size: 11px;
|
|
||||||
`}
|
|
||||||
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.hover &&
|
|
||||||
css`
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 1;
|
|
||||||
transition: 0.1s ease background-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--secondary-background);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.disabled &&
|
|
||||||
css`
|
|
||||||
opacity: 0.4;
|
|
||||||
/*.content,
|
|
||||||
.action {
|
|
||||||
color: var(--tertiary-foreground);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
.action {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.account &&
|
|
||||||
css`
|
|
||||||
height: 54px;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
.title {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
color: var(--foreground);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props extends BaseProps {
|
|
||||||
icon?: Children;
|
|
||||||
children?: Children;
|
|
||||||
description?: Children;
|
|
||||||
|
|
||||||
onClick?: () => void;
|
|
||||||
action?: "chevron" | "external" | Children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CategoryButton({
|
|
||||||
icon,
|
|
||||||
children,
|
|
||||||
description,
|
|
||||||
account,
|
|
||||||
disabled,
|
|
||||||
onClick,
|
|
||||||
hover,
|
|
||||||
action,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
|
||||||
<CategoryBase
|
|
||||||
hover={hover || typeof onClick !== "undefined"}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
account={account}>
|
|
||||||
{icon}
|
|
||||||
<div class="content">
|
|
||||||
<div className="title">{children}</div>
|
|
||||||
|
|
||||||
<div className="description">{description}</div>
|
|
||||||
</div>
|
|
||||||
<div class="action">
|
|
||||||
{typeof action === "string" ? (
|
|
||||||
action === "chevron" ? (
|
|
||||||
<ChevronRight size={24} />
|
|
||||||
) : (
|
|
||||||
<LinkExternal size={20} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
action
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CategoryBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,11 @@ import update from "dayjs/plugin/updateLocale";
|
|||||||
import defaultsDeep from "lodash.defaultsdeep";
|
import defaultsDeep from "lodash.defaultsdeep";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { IntlProvider } from "preact-i18n";
|
import { IntlProvider, Text } from "preact-i18n";
|
||||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Error } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useApplicationState } from "../mobx/State";
|
import { useApplicationState } from "../mobx/State";
|
||||||
|
|
||||||
import { Languages } from "../../external/lang/Languages";
|
import { Languages } from "../../external/lang/Languages";
|
||||||
@@ -143,3 +145,12 @@ function transformLanguage(source: Dictionary) {
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function I18nError({ error, children }: { error: any; children?: any }) {
|
||||||
|
return (
|
||||||
|
<Error
|
||||||
|
error={error ? <Text id={error} children={error} /> : undefined}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -336,9 +336,9 @@ export const generateVariables = (theme: Theme) => {
|
|||||||
if (colour) {
|
if (colour) {
|
||||||
const [r, g, b] = colour;
|
const [r, g, b] = colour;
|
||||||
return `--${key}: ${theme[key]}; --${key}-rgb: ${r}, ${g}, ${b};`;
|
return `--${key}: ${theme[key]}; --${key}-rgb: ${r}, ${g}, ${b};`;
|
||||||
} else {
|
|
||||||
return `--${key}: ${theme[key]};`;
|
|
||||||
}
|
}
|
||||||
|
return `--${key}: ${theme[key]};`;
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
5
src/context/history.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createBrowserHistory } from "history";
|
||||||
|
|
||||||
|
export const history = createBrowserHistory({
|
||||||
|
basename: import.meta.env.BASE_URL,
|
||||||
|
});
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
import { BrowserRouter as Router, Link } from "react-router-dom";
|
import { Router, Link } from "react-router-dom";
|
||||||
|
|
||||||
import { ContextMenuTrigger } from "preact-context-menu";
|
import { ContextMenuTrigger } from "preact-context-menu";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { LinkProvider, TextProvider, TrigProvider } from "@revoltchat/ui";
|
import { Preloader, UIProvider } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { hydrateState } from "../mobx/State";
|
import { hydrateState } from "../mobx/State";
|
||||||
|
|
||||||
import Preloader from "../components/ui/Preloader";
|
|
||||||
import { Children } from "../types/Preact";
|
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
import Theme from "./Theme";
|
import Theme from "./Theme";
|
||||||
|
import { history } from "./history";
|
||||||
import Intermediate from "./intermediate/Intermediate";
|
import Intermediate from "./intermediate/Intermediate";
|
||||||
|
import ModalRenderer from "./modals/ModalRenderer";
|
||||||
import Client from "./revoltjs/RevoltClient";
|
import Client from "./revoltjs/RevoltClient";
|
||||||
import SyncManager from "./revoltjs/SyncManager";
|
import SyncManager from "./revoltjs/SyncManager";
|
||||||
|
|
||||||
|
const uiContext = {
|
||||||
|
Link,
|
||||||
|
Text: Text as any,
|
||||||
|
Trigger: ContextMenuTrigger,
|
||||||
|
emitAction: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component provides all of the application's context layers.
|
* This component provides all of the application's context layers.
|
||||||
* @param param0 Provided children
|
* @param param0 Provided children
|
||||||
@@ -30,10 +37,8 @@ export default function Context({ children }: { children: Children }) {
|
|||||||
if (!ready) return <Preloader type="spinner" />;
|
if (!ready) return <Preloader type="spinner" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
<Router history={history}>
|
||||||
<LinkProvider value={Link}>
|
<UIProvider value={uiContext}>
|
||||||
<TextProvider value={Text as any}>
|
|
||||||
<TrigProvider value={ContextMenuTrigger}>
|
|
||||||
<Locale>
|
<Locale>
|
||||||
<Intermediate>
|
<Intermediate>
|
||||||
<Client>
|
<Client>
|
||||||
@@ -41,10 +46,9 @@ export default function Context({ children }: { children: Children }) {
|
|||||||
<SyncManager />
|
<SyncManager />
|
||||||
</Client>
|
</Client>
|
||||||
</Intermediate>
|
</Intermediate>
|
||||||
|
<ModalRenderer />
|
||||||
</Locale>
|
</Locale>
|
||||||
</TrigProvider>
|
</UIProvider>
|
||||||
</TextProvider>
|
|
||||||
</LinkProvider>
|
|
||||||
<Theme />
|
<Theme />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,22 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { API, Channel, Message, Server, User } from "revolt.js";
|
import { API, Channel, Message, Server, User } from "revolt.js";
|
||||||
|
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
import {
|
||||||
|
StateUpdater,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "preact/hooks";
|
||||||
|
|
||||||
|
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||||
import { determineLink } from "../../lib/links";
|
import { determineLink } from "../../lib/links";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { Action } from "../../components/ui/Modal";
|
import { modalController } from "../modals";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
import Modals from "./Modals";
|
import Modals from "./Modals";
|
||||||
|
|
||||||
export type Screen =
|
export type Screen =
|
||||||
@@ -24,7 +30,7 @@ export type Screen =
|
|||||||
| { id: "clipboard"; text: string }
|
| { id: "clipboard"; text: string }
|
||||||
| { id: "token_reveal"; token: string; username: string }
|
| { id: "token_reveal"; token: string; username: string }
|
||||||
| { id: "external_link_prompt"; link: string }
|
| { id: "external_link_prompt"; link: string }
|
||||||
| { id: "sessions", confirm: () => void }
|
| { id: "sessions"; confirm: () => void }
|
||||||
| {
|
| {
|
||||||
id: "_prompt";
|
id: "_prompt";
|
||||||
question: Children;
|
question: Children;
|
||||||
@@ -128,9 +134,11 @@ interface Props {
|
|||||||
children: Children;
|
children: Children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let __thisIsAHack: StateUpdater<Screen>;
|
||||||
|
|
||||||
export default function Intermediate(props: Props) {
|
export default function Intermediate(props: Props) {
|
||||||
const [screen, openScreen] = useState<Screen>({ id: "none" });
|
const [screen, openScreen] = useState<Screen>({ id: "none" });
|
||||||
const settings = useApplicationState().settings;
|
__thisIsAHack = openScreen;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
@@ -141,44 +149,10 @@ export default function Intermediate(props: Props) {
|
|||||||
const actions = useMemo(() => {
|
const actions = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
openLink: (href?: string, trusted?: boolean) => {
|
openLink: (href?: string, trusted?: boolean) => {
|
||||||
const link = determineLink(href);
|
return modalController.openLink(href, trusted);
|
||||||
|
|
||||||
switch (link.type) {
|
|
||||||
case "profile": {
|
|
||||||
openScreen({ id: "profile", user_id: link.id });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "navigate": {
|
|
||||||
history.push(link.path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "external": {
|
|
||||||
if (
|
|
||||||
!trusted &&
|
|
||||||
!settings.security.isTrustedOrigin(
|
|
||||||
link.url.hostname,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
openScreen({
|
|
||||||
id: "external_link_prompt",
|
|
||||||
link: link.href,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.open(link.href, "_blank", "noreferrer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
openScreen: (screen: Screen) => openScreen(screen),
|
openScreen: (screen: Screen) => openScreen(screen),
|
||||||
writeClipboard: (text: string) => {
|
writeClipboard: (a: string) => modalController.writeText(a),
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
} else {
|
|
||||||
actions.openScreen({ id: "clipboard", text });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import { internalEmit } from "../../lib/eventEmitter";
|
//import { isModalClosing } from "../../components/ui/Modal";
|
||||||
|
|
||||||
import { isModalClosing } from "../../components/ui/Modal";
|
|
||||||
|
|
||||||
import { Screen } from "./Intermediate";
|
import { Screen } from "./Intermediate";
|
||||||
import { ClipboardModal } from "./modals/Clipboard";
|
|
||||||
import { ErrorModal } from "./modals/Error";
|
|
||||||
import { InputModal } from "./modals/Input";
|
import { InputModal } from "./modals/Input";
|
||||||
import { OnboardingModal } from "./modals/Onboarding";
|
import { OnboardingModal } from "./modals/Onboarding";
|
||||||
import { PromptModal } from "./modals/Prompt";
|
import { PromptModal } from "./modals/Prompt";
|
||||||
import { SignedOutModal } from "./modals/SignedOut";
|
|
||||||
import { ExternalLinkModal} from "./modals/ExternalLinkPrompt";
|
|
||||||
import { SessionsModal } from "./modals/SessionsPrompt";
|
|
||||||
import { TokenRevealModal } from "./modals/TokenReveal";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
screen: Screen;
|
screen: Screen;
|
||||||
@@ -20,29 +11,17 @@ export interface Props {
|
|||||||
|
|
||||||
export default function Modals({ screen, openScreen }: Props) {
|
export default function Modals({ screen, openScreen }: Props) {
|
||||||
const onClose = () =>
|
const onClose = () =>
|
||||||
isModalClosing || screen.id === "onboarding"
|
//isModalClosing || screen.id === "onboarding"
|
||||||
? openScreen({ id: "none" })
|
openScreen({ id: "none" });
|
||||||
: internalEmit("Modal", "close");
|
// : internalEmit("Modal", "close");
|
||||||
|
|
||||||
switch (screen.id) {
|
switch (screen.id) {
|
||||||
case "_prompt":
|
case "_prompt":
|
||||||
return <PromptModal onClose={onClose} {...screen} />;
|
return <PromptModal onClose={onClose} {...screen} />;
|
||||||
case "_input":
|
case "_input":
|
||||||
return <InputModal onClose={onClose} {...screen} />;
|
return <InputModal onClose={onClose} {...screen} />;
|
||||||
case "error":
|
|
||||||
return <ErrorModal onClose={onClose} {...screen} />;
|
|
||||||
case "signed_out":
|
|
||||||
return <SignedOutModal onClose={onClose} {...screen} />;
|
|
||||||
case "clipboard":
|
|
||||||
return <ClipboardModal onClose={onClose} {...screen} />;
|
|
||||||
case "token_reveal":
|
|
||||||
return <TokenRevealModal onClose={onClose} {...screen} />;
|
|
||||||
case "onboarding":
|
case "onboarding":
|
||||||
return <OnboardingModal onClose={onClose} {...screen} />;
|
return <OnboardingModal onClose={onClose} {...screen} />;
|
||||||
case "external_link_prompt":
|
|
||||||
return <ExternalLinkModal onClose={onClose} {...screen} />;
|
|
||||||
case "sessions":
|
|
||||||
return <SessionsModal onClose={onClose} {...screen} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { internalEmit } from "../../lib/eventEmitter";
|
|
||||||
|
|
||||||
import { isModalClosing } from "../../components/ui/Modal";
|
|
||||||
|
|
||||||
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||||
import { SpecialInputModal } from "./modals/Input";
|
import { SpecialInputModal } from "./modals/Input";
|
||||||
import { SpecialPromptModal } from "./modals/Prompt";
|
import { SpecialPromptModal } from "./modals/Prompt";
|
||||||
import { ChannelInfo } from "./popovers/ChannelInfo";
|
import { ChannelInfo } from "./popovers/ChannelInfo";
|
||||||
import { CreateBotModal } from "./popovers/CreateBot";
|
import { CreateBotModal } from "./popovers/CreateBot";
|
||||||
import { ImageViewer } from "./popovers/ImageViewer";
|
import { ImageViewer } from "./popovers/ImageViewer";
|
||||||
import { ModifyAccountModal } from "./popovers/ModifyAccount";
|
|
||||||
import { PendingRequests } from "./popovers/PendingRequests";
|
|
||||||
import { ServerIdentityModal } from "./popovers/ServerIdentityModal";
|
|
||||||
import { UserPicker } from "./popovers/UserPicker";
|
import { UserPicker } from "./popovers/UserPicker";
|
||||||
import { UserProfile } from "./popovers/UserProfile";
|
import { UserProfile } from "./popovers/UserProfile";
|
||||||
|
|
||||||
@@ -21,9 +14,9 @@ export default function Popovers() {
|
|||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
|
|
||||||
const onClose = () =>
|
const onClose = () =>
|
||||||
isModalClosing
|
//isModalClosing
|
||||||
? openScreen({ id: "none" })
|
openScreen({ id: "none" });
|
||||||
: internalEmit("Modal", "close");
|
//: internalEmit("Modal", "close");
|
||||||
|
|
||||||
switch (screen.id) {
|
switch (screen.id) {
|
||||||
case "profile":
|
case "profile":
|
||||||
@@ -37,12 +30,6 @@ export default function Popovers() {
|
|||||||
case "channel_info":
|
case "channel_info":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
return <ChannelInfo {...screen} onClose={onClose} />;
|
||||||
case "pending_requests":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <PendingRequests {...screen} onClose={onClose} />;
|
|
||||||
case "modify_account":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <ModifyAccountModal onClose={onClose} {...screen} />;
|
|
||||||
case "create_bot":
|
case "create_bot":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <CreateBotModal onClose={onClose} {...screen} />;
|
return <CreateBotModal onClose={onClose} {...screen} />;
|
||||||
@@ -52,9 +39,6 @@ export default function Popovers() {
|
|||||||
case "special_input":
|
case "special_input":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <SpecialInputModal onClose={onClose} {...screen} />;
|
return <SpecialInputModal onClose={onClose} {...screen} />;
|
||||||
case "server_identity":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <ServerIdentityModal onClose={onClose} {...screen} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
import { Server } from "revolt.js";
|
import { Server } from "revolt.js";
|
||||||
import { ulid } from "ulid";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useContext, useState } from "preact/hooks";
|
||||||
|
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import { Category, InputBox, Modal } from "@revoltchat/ui";
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
import Overline from "../../../components/ui/Overline";
|
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
import { I18nError } from "../../Locale";
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
|
|
||||||
@@ -36,7 +33,6 @@ export function InputModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
|
||||||
title={question}
|
title={question}
|
||||||
description={description}
|
description={description}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -61,11 +57,15 @@ export function InputModal({
|
|||||||
]}
|
]}
|
||||||
onClose={onClose}>
|
onClose={onClose}>
|
||||||
{field ? (
|
{field ? (
|
||||||
<Overline error={error} block>
|
<Category>
|
||||||
{field}
|
<I18nError error={error}>{field}</I18nError>
|
||||||
</Overline>
|
</Category>
|
||||||
) : (
|
) : (
|
||||||
error && <Overline error={error} type="error" block />
|
error && (
|
||||||
|
<Category>
|
||||||
|
<I18nError error={error} />
|
||||||
|
</Category>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
<InputBox
|
<InputBox
|
||||||
value={value}
|
value={value}
|
||||||
@@ -177,7 +177,7 @@ export function SpecialInputModal(props: SpecialProps) {
|
|||||||
question={"Add Friend"}
|
question={"Add Friend"}
|
||||||
callback={(username) =>
|
callback={(username) =>
|
||||||
client.api
|
client.api
|
||||||
.put(`/users/${username as ""}/friend`)
|
.post(`/users/friend`, { username })
|
||||||
.then(undefined)
|
.then(undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import styles from "./Onboarding.module.scss";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import Preloader from "../../../components/ui/Preloader";
|
|
||||||
import wideSVG from "/assets/wide.svg";
|
import wideSVG from "/assets/wide.svg";
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
import FormField from "../../../pages/login/FormField";
|
||||||
|
|||||||
@@ -7,16 +7,14 @@ import styles from "./Prompt.module.scss";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category, Modal, InputBox, Radio } from "@revoltchat/ui";
|
||||||
|
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
||||||
|
|
||||||
import { TextReact } from "../../../lib/i18n";
|
import { TextReact } from "../../../lib/i18n";
|
||||||
|
|
||||||
import Message from "../../../components/common/messaging/Message";
|
import Message from "../../../components/common/messaging/Message";
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import { I18nError } from "../../Locale";
|
||||||
import Modal, { Action } from "../../../components/ui/Modal";
|
|
||||||
import Overline from "../../../components/ui/Overline";
|
|
||||||
import Radio from "../../../components/ui/Radio";
|
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
import { useIntermediate } from "../Intermediate";
|
import { useIntermediate } from "../Intermediate";
|
||||||
@@ -40,12 +38,15 @@ export function PromptModal({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
|
||||||
title={question}
|
title={question}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
{error && <Overline error={error} type="error" />}
|
{error && (
|
||||||
|
<Category>
|
||||||
|
<I18nError error={error} />
|
||||||
|
</Category>
|
||||||
|
)}
|
||||||
{content}
|
{content}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -167,10 +168,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -209,10 +211,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
props.target.delete();
|
props.target.delete();
|
||||||
onClose();
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -315,10 +318,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
})
|
})
|
||||||
?.kick();
|
?.kick();
|
||||||
|
|
||||||
onClose();
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,10 +369,12 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
await props.target.banUser(props.user._id, {
|
await props.target.banUser(props.user._id, {
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
onClose();
|
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -386,9 +392,9 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
id="app.special.modals.prompt.confirm_ban"
|
id="app.special.modals.prompt.confirm_ban"
|
||||||
fields={{ name: props.user?.username }}
|
fields={{ name: props.user?.username }}
|
||||||
/>
|
/>
|
||||||
<Overline>
|
<Category>
|
||||||
<Text id="app.special.modals.prompt.confirm_ban_reason" />
|
<Text id="app.special.modals.prompt.confirm_ban_reason" />
|
||||||
</Overline>
|
</Category>
|
||||||
<InputBox
|
<InputBox
|
||||||
value={reason ?? ""}
|
value={reason ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -437,10 +443,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -453,22 +460,26 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
]}
|
]}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
<Overline block type="subtle">
|
<Category>
|
||||||
<Text id="app.main.servers.channel_type" />
|
<Text id="app.main.servers.channel_type" />
|
||||||
</Overline>
|
</Category>
|
||||||
<Radio
|
<Radio
|
||||||
checked={type === "Text"}
|
title={
|
||||||
onSelect={() => setType("Text")}>
|
|
||||||
<Text id="app.main.servers.text_channel" />
|
<Text id="app.main.servers.text_channel" />
|
||||||
</Radio>
|
}
|
||||||
|
value={type === "Text"}
|
||||||
|
onSelect={() => setType("Text")}
|
||||||
|
/>
|
||||||
<Radio
|
<Radio
|
||||||
checked={type === "Voice"}
|
title={
|
||||||
onSelect={() => setType("Voice")}>
|
|
||||||
<Text id="app.main.servers.voice_channel" />
|
<Text id="app.main.servers.voice_channel" />
|
||||||
</Radio>
|
}
|
||||||
<Overline block type="subtle">
|
value={type === "Voice"}
|
||||||
|
onSelect={() => setType("Voice")}
|
||||||
|
/>
|
||||||
|
<Category>
|
||||||
<Text id="app.main.servers.channel_name" />
|
<Text id="app.main.servers.channel_name" />
|
||||||
</Overline>
|
</Category>
|
||||||
<InputBox
|
<InputBox
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
onChange={(e) => setName(e.currentTarget.value)}
|
||||||
@@ -507,11 +518,13 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
onClose();
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -524,9 +537,9 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|||||||
]}
|
]}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
<Overline block type="subtle">
|
<Category>
|
||||||
<Text id="app.main.servers.category_name" />
|
<Text id="app.main.servers.category_name" />
|
||||||
</Overline>
|
</Category>
|
||||||
<InputBox
|
<InputBox
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
onChange={(e) => setName(e.currentTarget.value)}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Text } from "preact-i18n";
|
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
token: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TokenRevealModal({ onClose, token, username }: Props) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={true}
|
|
||||||
onClose={onClose}
|
|
||||||
title={
|
|
||||||
<Text
|
|
||||||
id={"app.special.modals.token_reveal"}
|
|
||||||
fields={{ name: username }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
onClick: onClose,
|
|
||||||
confirmation: true,
|
|
||||||
children: <Text id="app.special.modals.actions.close" />,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<code style={{ userSelect: "all" }}>{token}</code>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { Channel } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./ChannelInfo.module.scss";
|
import styles from "./ChannelInfo.module.scss";
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
import { getChannelName } from "../../revoltjs/util";
|
import { getChannelName } from "../../revoltjs/util";
|
||||||
@@ -24,7 +24,7 @@ export const ChannelInfo = observer(({ channel, onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={true} onClose={onClose}>
|
<Modal onClose={onClose}>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h1>{getChannelName(channel, true)}</h1>
|
<h1>{getChannelName(channel, true)}</h1>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { API } from "revolt.js";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useContext, useState } from "preact/hooks";
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
import { Category, Modal } from "@revoltchat/ui";
|
||||||
import Overline from "../../../components/ui/Overline";
|
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
import FormField from "../../../pages/login/FormField";
|
||||||
|
import { I18nError } from "../../Locale";
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
|
|
||||||
@@ -37,14 +37,16 @@ export function CreateBotModal({ onClose, onCreate }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={<Text id="app.special.popovers.create_bot.title" />}
|
title={<Text id="app.special.popovers.create_bot.title" />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
palette: "accent",
|
palette: "accent",
|
||||||
onClick: handleSubmit(onSubmit),
|
onClick: async () => {
|
||||||
|
await handleSubmit(onSubmit)();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
children: <Text id="app.special.modals.actions.create" />,
|
children: <Text id="app.special.modals.actions.create" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,9 +72,10 @@ export function CreateBotModal({ onClose, onCreate }: Props) {
|
|||||||
error={errors.name?.message}
|
error={errors.name?.message}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<Overline type="error" error={error}>
|
<Category>
|
||||||
<Text id="app.special.popovers.create_bot.failed" />
|
<Text id="app.special.popovers.create_bot.failed" />{" "}
|
||||||
</Overline>
|
· <I18nError error={error} />
|
||||||
|
</Category>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { API } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./ImageViewer.module.scss";
|
import styles from "./ImageViewer.module.scss";
|
||||||
|
|
||||||
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
||||||
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
|
|
||||||
import { useClient } from "../../revoltjs/RevoltClient";
|
import { useClient } from "../../revoltjs/RevoltClient";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -28,7 +28,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={true} onClose={onClose} noBackground>
|
<Modal onClose={onClose} transparent maxHeight="100vh" maxWidth="100vw">
|
||||||
<div className={styles.viewer}>
|
<div className={styles.viewer}>
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { User } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./UserPicker.module.scss";
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
|
|
||||||
import { Friend } from "../../../pages/friends/Friend";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
users: User[];
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PendingRequests = observer(({ users, onClose }: Props) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={true}
|
|
||||||
title={<Text id="app.special.friends.pending" />}
|
|
||||||
onClose={onClose}>
|
|
||||||
<div className={styles.list}>
|
|
||||||
{users.map((x) => (
|
|
||||||
<Friend user={x!} key={x!._id} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.identityMain {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Server } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ServerIdentityModal.module.scss";
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
import { useEffect, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import { noop } from "../../../lib/js";
|
|
||||||
|
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
import Overline from "../../../components/ui/Overline";
|
|
||||||
|
|
||||||
import { FileUploader } from "../../revoltjs/FileUploads";
|
|
||||||
import { useClient } from "../../revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
server: Server;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ServerIdentityModal = observer(({ server, onClose }: Props) => {
|
|
||||||
const client = useClient();
|
|
||||||
const member = client.members.getKey({
|
|
||||||
server: server._id,
|
|
||||||
user: client.user!._id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!member) return null;
|
|
||||||
|
|
||||||
const [nickname, setNickname] = useState("");
|
|
||||||
const [currentNickname, setCurrentNickname] = useState("");
|
|
||||||
useEffect(() => {
|
|
||||||
setNickname(member.nickname ?? "");
|
|
||||||
setCurrentNickname(member.nickname ?? "");
|
|
||||||
}, [member.nickname]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={true}
|
|
||||||
title={
|
|
||||||
<Text
|
|
||||||
id={"app.special.popovers.server_identity.title"}
|
|
||||||
fields={{ server: server.name }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClose={onClose}>
|
|
||||||
<div className={styles.identityMain}>
|
|
||||||
<div>
|
|
||||||
<Overline type="subtle">
|
|
||||||
<Text id="app.special.popovers.server_identity.avatar" />
|
|
||||||
</Overline>
|
|
||||||
<FileUploader
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
style="icon"
|
|
||||||
fileType="avatars"
|
|
||||||
behaviour="upload"
|
|
||||||
maxFileSize={4_000_000}
|
|
||||||
onUpload={(avatar) =>
|
|
||||||
member.edit({ avatar }).then(noop)
|
|
||||||
}
|
|
||||||
remove={() =>
|
|
||||||
member.edit({ remove: ["Avatar"] }).then(noop)
|
|
||||||
}
|
|
||||||
defaultPreview={client.user?.generateAvatarURL(
|
|
||||||
{
|
|
||||||
max_side: 256,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
)}
|
|
||||||
previewURL={client.generateFileURL(
|
|
||||||
member.avatar ?? undefined,
|
|
||||||
{ max_side: 256 },
|
|
||||||
true,
|
|
||||||
)}
|
|
||||||
desaturateDefault
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Overline type="subtle">
|
|
||||||
<Text id="app.special.popovers.server_identity.nickname" />
|
|
||||||
</Overline>
|
|
||||||
<InputBox
|
|
||||||
value={nickname}
|
|
||||||
placeholder={client.user!.username}
|
|
||||||
onChange={(e) => setNickname(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
<Button
|
|
||||||
disabled={nickname === currentNickname}
|
|
||||||
onClick={() => member.edit({ nickname })}>
|
|
||||||
<Text id="app.special.modals.actions.save" />
|
|
||||||
</Button>
|
|
||||||
{currentNickname !== "" && (
|
|
||||||
<Button
|
|
||||||
palette="plain"
|
|
||||||
onClick={() =>
|
|
||||||
member.edit({ remove: ["Nickname"] })
|
|
||||||
}>
|
|
||||||
<Text id="app.special.modals.actions.remove" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
.list {
|
.list {
|
||||||
width: 400px;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
> label {
|
|
||||||
> span {
|
|
||||||
align-items: flex-start !important;
|
|
||||||
> span {
|
|
||||||
display: flex;
|
|
||||||
padding: 4px;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import styles from "./UserPicker.module.scss";
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
import { Modal } from "@revoltchat/ui";
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
|
|
||||||
|
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
||||||
import { useClient } from "../../revoltjs/RevoltClient";
|
import { useClient } from "../../revoltjs/RevoltClient";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -21,13 +21,12 @@ export function UserPicker(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
|
||||||
title={<Text id="app.special.popovers.user_picker.select" />}
|
title={<Text id="app.special.popovers.user_picker.select" />}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
children: <Text id="app.special.modals.actions.ok" />,
|
children: <Text id="app.special.modals.actions.ok" />,
|
||||||
onClick: () => props.callback(selected).then(props.onClose),
|
onClick: () => props.callback(selected).then(() => true),
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
@@ -42,7 +41,7 @@ export function UserPicker(props: Props) {
|
|||||||
<UserCheckbox
|
<UserCheckbox
|
||||||
key={x._id}
|
key={x._id}
|
||||||
user={x}
|
user={x}
|
||||||
checked={selected.includes(x._id)}
|
value={selected.includes(x._id)}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
setSelected([...selected, x._id]);
|
setSelected([...selected, x._id]);
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ import styles from "./UserProfile.module.scss";
|
|||||||
import { Localizer, Text } from "preact-i18n";
|
import { Localizer, Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Category,
|
||||||
|
Error,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
Preloader,
|
||||||
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
import { noop } from "../../../lib/js";
|
import { noop } from "../../../lib/js";
|
||||||
|
|
||||||
@@ -26,11 +33,6 @@ import UserBadges from "../../../components/common/user/UserBadges";
|
|||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
import { Username } from "../../../components/common/user/UserShort";
|
import { Username } from "../../../components/common/user/UserShort";
|
||||||
import UserStatus from "../../../components/common/user/UserStatus";
|
import UserStatus from "../../../components/common/user/UserStatus";
|
||||||
import IconButton from "../../../components/ui/IconButton";
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
|
||||||
import Overline from "../../../components/ui/Overline";
|
|
||||||
import Preloader from "../../../components/ui/Preloader";
|
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
import {
|
import {
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
@@ -147,13 +149,8 @@ export const UserProfile = observer(
|
|||||||
const badges = user.badges ?? 0;
|
const badges = user.badges ?? 0;
|
||||||
const flags = user.flags ?? 0;
|
const flags = user.flags ?? 0;
|
||||||
|
|
||||||
return (
|
const children = (
|
||||||
<Modal
|
<>
|
||||||
visible
|
|
||||||
border={dummy}
|
|
||||||
padding={false}
|
|
||||||
onClose={onClose}
|
|
||||||
dontModal={dummy}>
|
|
||||||
<div
|
<div
|
||||||
className={styles.header}
|
className={styles.header}
|
||||||
data-force={profile?.background ? "light" : undefined}
|
data-force={profile?.background ? "light" : undefined}
|
||||||
@@ -281,19 +278,19 @@ export const UserProfile = observer(
|
|||||||
<div>
|
<div>
|
||||||
{flags & 1 ? (
|
{flags & 1 ? (
|
||||||
/** ! FIXME: i18n this area */
|
/** ! FIXME: i18n this area */
|
||||||
<Overline type="error" block>
|
<Category>
|
||||||
User is suspended
|
<Error error="User is suspended" />
|
||||||
</Overline>
|
</Category>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{flags & 2 ? (
|
{flags & 2 ? (
|
||||||
<Overline type="error" block>
|
<Category>
|
||||||
User deleted their account
|
<Error error="User deleted their account" />
|
||||||
</Overline>
|
</Category>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{flags & 4 ? (
|
{flags & 4 ? (
|
||||||
<Overline type="error" block>
|
<Category>
|
||||||
User is banned
|
<Error error="User is banned" />
|
||||||
</Overline>
|
</Category>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{user.bot ? (
|
{user.bot ? (
|
||||||
<>
|
<>
|
||||||
@@ -441,6 +438,18 @@ export const UserProfile = observer(
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dummy) return <div>{children}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onClose={onClose}
|
||||||
|
nonDismissable={dummy}
|
||||||
|
transparent
|
||||||
|
maxWidth="560px">
|
||||||
|
{children}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
22
src/context/modals/ModalRenderer.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { modalController } from ".";
|
||||||
|
|
||||||
|
export default observer(() => {
|
||||||
|
useEffect(() => {
|
||||||
|
function keyUp(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
modalController.pop("close");
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
modalController.pop("confirm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keyup", keyUp);
|
||||||
|
return () => document.removeEventListener("keyup", keyUp);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return modalController.rendered;
|
||||||
|
});
|
||||||
107
src/context/modals/components/Changelog.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { CategoryButton, Column, Modal } from "@revoltchat/ui";
|
||||||
|
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
||||||
|
|
||||||
|
import { noopTrue } from "../../../lib/js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
changelogEntries,
|
||||||
|
changelogEntryArray,
|
||||||
|
ChangelogPost,
|
||||||
|
} from "../../../assets/changelogs";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
const Image = styled.img`
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
function RenderLog({ post }: { post: ChangelogPost }) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
{post.content.map((entry) =>
|
||||||
|
typeof entry === "string" ? (
|
||||||
|
<span>{entry}</span>
|
||||||
|
) : (
|
||||||
|
<Image src={entry.src} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changelog modal
|
||||||
|
*/
|
||||||
|
export default function Changelog({
|
||||||
|
initial,
|
||||||
|
onClose,
|
||||||
|
signal,
|
||||||
|
}: ModalProps<"changelog">) {
|
||||||
|
const [log, setLog] = useState(initial);
|
||||||
|
|
||||||
|
const entry = useMemo(
|
||||||
|
() => (log ? changelogEntries[log] : undefined),
|
||||||
|
[log],
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = useMemo(() => {
|
||||||
|
const arr: Action[] = [
|
||||||
|
{
|
||||||
|
palette: "primary",
|
||||||
|
children: <Text id="app.special.modals.actions.close" />,
|
||||||
|
onClick: noopTrue,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (log) {
|
||||||
|
arr.push({
|
||||||
|
palette: "plain-secondary",
|
||||||
|
children: <Text id="app.special.modals.changelogs.older" />,
|
||||||
|
onClick: () => {
|
||||||
|
setLog(undefined);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, [log]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
entry?.title ?? (
|
||||||
|
<Text id="app.special.modals.changelogs.title" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
entry ? (
|
||||||
|
dayjs(entry.date).calendar()
|
||||||
|
) : (
|
||||||
|
<Text id="app.special.modals.changelogs.description" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actions={actions}
|
||||||
|
onClose={onClose}
|
||||||
|
signal={signal}>
|
||||||
|
{entry ? (
|
||||||
|
<RenderLog post={entry} />
|
||||||
|
) : (
|
||||||
|
<Column>
|
||||||
|
{changelogEntryArray.map((entry, index) => (
|
||||||
|
<CategoryButton
|
||||||
|
key={index}
|
||||||
|
onClick={() => setLog(index + 1)}>
|
||||||
|
{entry.title}
|
||||||
|
</CategoryButton>
|
||||||
|
))}
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
interface Props {
|
import { noopTrue } from "../../../lib/js";
|
||||||
onClose: () => void;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClipboardModal({ onClose, text }: Props) {
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
export default function Clipboard({ text, ...props }: ModalProps<"clipboard">) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
{...props}
|
||||||
onClose={onClose}
|
|
||||||
title={<Text id="app.special.modals.clipboard.unavailable" />}
|
title={<Text id="app.special.modals.clipboard.unavailable" />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
onClick: onClose,
|
onClick: noopTrue,
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
children: <Text id="app.special.modals.actions.close" />,
|
children: <Text id="app.special.modals.actions.close" />,
|
||||||
},
|
},
|
||||||
@@ -26,7 +24,9 @@ export function ClipboardModal({ onClose, text }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<Text id="app.special.modals.clipboard.copy" />{" "}
|
<Text id="app.special.modals.clipboard.copy" />{" "}
|
||||||
<code style={{ userSelect: "all" }}>{text}</code>
|
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||||
|
{text}
|
||||||
|
</code>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,24 @@
|
|||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import Modal from "../../../components/ui/Modal";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
interface Props {
|
import { noopTrue } from "../../../lib/js";
|
||||||
onClose: () => void;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorModal({ onClose, error }: Props) {
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
export default function Error({ error, ...props }: ModalProps<"error">) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={true}
|
{...props}
|
||||||
onClose={onClose}
|
|
||||||
title={<Text id="app.special.modals.error" />}
|
title={<Text id="app.special.modals.error" />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
onClick: onClose,
|
onClick: noopTrue,
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
children: <Text id="app.special.modals.actions.ok" />,
|
children: <Text id="app.special.modals.actions.ok" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
palette: "plain-secondary",
|
||||||
onClick: () => location.reload(),
|
onClick: () => location.reload(),
|
||||||
children: <Text id="app.special.modals.actions.reload" />,
|
children: <Text id="app.special.modals.actions.reload" />,
|
||||||
},
|
},
|
||||||