83 Commits

Author SHA1 Message Date
Paul Makles
95ebd935ed fix: duct-tape fix the bot edit issues
fixes #629
2022-06-21 11:14:51 +01:00
Paul Makles
3b7c1cbe20 chore(ci): allow any tag [skip ci] 2022-06-21 11:14:04 +01:00
Paul Makles
cb0a521473 fix: no client context on ModifyAccount
fix: no reactivity on account settings

closes #706
fixes #683
fixes #702
2022-06-21 10:57:58 +01:00
Paul Makles
aa9974149c fix: bump @revoltchat/ui 2022-06-18 17:13:00 +01:00
Paul Makles
569864167e fix: consider whether alert is present in height calc 2022-06-18 17:05:05 +01:00
Paul Makles
f185dec461 feat: handle system alerts and poll rate changes 2022-06-18 17:03:04 +01:00
Paul Makles
03e177f865 feat(modal): implement new server identity modal
closes #172
2022-06-18 15:54:17 +01:00
Paul Makles
f685352963 fix: temporarily hack in mention to profile flow 2022-06-18 15:06:24 +01:00
Paul Makles
6755217ad2 feat(modal): port ModifyAccount and PendingRequest 2022-06-18 15:02:59 +01:00
Paul Makles
211ff2058a chore: remove legacy Redux migration 2022-06-18 14:49:59 +01:00
Paul Makles
b7be9f8c03 feat(modal): port LinkWarning 2022-06-18 14:19:31 +01:00
Paul Makles
241b9cd27b feat(modal): port Clipboard 2022-06-18 12:33:22 +01:00
Paul Makles
d10bd96900 feat(modal): port Error and ShowToken 2022-06-18 12:25:56 +01:00
Paul Makles
374be319c4 feat(modals): port SignedOut and SignOutSessions 2022-06-18 11:56:05 +01:00
Paul Makles
0ee7b73d61 feat: re-work modal behaviour to be more natural 2022-06-18 11:22:37 +01:00
Paul Makles
63d5f6bb7d fix: show update button on native if frame off 2022-06-17 16:57:13 +01:00
Leda
34e6995d86 fix: modify account, and create bot forms not being submitted (#697) 2022-06-17 16:51:10 +01:00
Paul Makles
a1ef1dce5e fix: remove status request
closes #685
closes #643
2022-06-17 16:50:48 +01:00
Paul Makles
a190a51d0b fix(qr): render the QR code consistently 2022-06-14 18:01:19 +01:00
Paul Makles
c9127d6cf3 fix(css): please let the torture stop 2022-06-14 17:21:52 +01:00
div2005
f0d2e31b17 fix: bottom nav alignment is wrong (#684)
Co-authored-by: div2005 <div2005@tuta.io>
Co-authored-by: Paul Makles <paulmakles@gmail.com>
2022-06-14 17:19:18 +01:00
Paul Makles
220a28a151 fix: bottom nav button alignment
fixes #679
2022-06-14 17:16:18 +01:00
Paul Makles
b44779c89a fix: user picker checkbox + width
fixes #657
fixes #681
2022-06-14 17:10:59 +01:00
Paul Makles
5835064219 chore: fix merge conflicts 2022-06-14 16:30:42 +01:00
Paul Makles
b9da79bc11 chore: bump preact-context-menu 2022-06-14 16:27:46 +01:00
Leda
ba99cbaf2a fix: bug in user/channel mention when query text is empty (#659) 2022-06-14 15:24:13 +01:00
Leda
fc0c7611d4 fix: bug where channel icon scales with channel name (#661)
Co-authored-by: Paul Makles <paulmakles@gmail.com>
2022-06-14 15:23:58 +01:00
Leda
2e9c013ed8 fix: display voice channel as link in messages (#658)
* fix: display voice channel as link in messages

* chore: format
2022-06-14 15:17:00 +01:00
Leda
3e40a61624 fix: bug in join server button when in light theme (#660) 2022-06-14 15:13:31 +01:00
Paul Makles
4e22ccb2f7 chore: duct-tape fix for semver library breaking 2022-06-12 22:24:29 +01:00
Paul Makles
00718245f9 fix: make the changelog button in settings work 2022-06-12 22:21:23 +01:00
Paul Makles
cd8ab6739b feat: add changelog modal 2022-06-12 22:19:41 +01:00
Paul Makles
5eabd2861f chore: unlink components 2022-06-12 21:19:27 +01:00
Paul Makles
a404ff7fe0 feat: add auto-update and out-of-date indicator 2022-06-12 21:16:42 +01:00
Paul Makles
7680931f5f chore: i18n 2022-06-12 21:11:23 +01:00
Paul Makles
c1324108e3 fix(eslint): rules included deprecated plugin 2022-06-12 19:38:29 +01:00
Paul Makles
56770d40df chore: bump language submodule 2022-06-12 19:29:17 +01:00
Paul Makles
64f19ec2c0 chore: display error on load if present 2022-06-12 19:27:18 +01:00
Paul Makles
dbb1c1e8fa feat: finalise 2FA login 2022-06-12 19:24:59 +01:00
Paul Makles
c686e85d37 feat: add MFA recovery codes 2022-06-12 16:30:37 +01:00
Paul Makles
8eefc87b05 chore(refactor): clean up component folder structure 2022-06-12 15:24:00 +01:00
Paul Makles
645e1af6db chore: refactor account UI 2022-06-12 15:07:30 +01:00
Paul Makles
8103cc03cf chore: move "resend verification" button 2022-06-12 12:16:15 +01:00
Paul Makles
bdc527ebbe chore(ci): re-enable mirroring 2022-06-10 18:50:14 +01:00
Paul Makles
8a2826da91 fix: use class in markdown rendering 2022-06-10 17:36:59 +01:00
Paul Makles
bd50378234 feat: add account deletion confirmation route 2022-06-10 17:20:31 +01:00
Paul Makles
71f8fc86a4 chore: fix build errors 2022-06-10 17:00:37 +01:00
Paul Makles
277eaa685d chore(ci): stop mirroring to GitLab [skip ci] 2022-06-10 16:53:02 +01:00
Paul Makles
e81b8ed472 feat: new modal renderer + mfa flow modal 2022-06-10 16:52:12 +01:00
Paul Makles
6be0807433 feat: add disable / delete funct; bump revolt-api 2022-06-10 14:32:21 +01:00
Paul Makles
e0ca1681bd chore(refactor): move and re-organise types folder 2022-06-10 14:11:38 +01:00
Paul Makles
ebcbe4bd4b chore: read version from package.json 2022-06-10 12:18:02 +01:00
Paul Makles
ec8b51f559 chore: repository clean-up 2022-06-09 14:58:39 +01:00
Paul Makles
72e60b7528 chore: clean up build errors 2022-06-09 14:50:05 +01:00
Paul Makles
3399991824 merge: pull request #650 from revoltchat/chore/component-migration 2022-06-09 14:49:17 +01:00
Paul Makles
6497e11fb0 Merge branch 'master' into chore/component-migration 2022-06-09 14:48:45 +01:00
Paul Makles
7544c78360 chore: prepare account management buttons 2022-06-09 14:47:53 +01:00
Paul Makles
040c4367f7 chore: clean up code 2022-06-09 14:47:33 +01:00
Paul Makles
ee80dfd3c8 fix: pull Client into state early
(should fix empty server list)
2022-06-09 14:40:54 +01:00
Paul Makles
67c8418c31 fix: actually resolve the error from requests 2022-06-07 17:00:11 +01:00
Paul Makles
2056232759 fix: build errors 2022-05-30 16:46:07 +01:00
Paul Makles
81bf325990 feat: add column element 2022-05-30 16:15:52 +01:00
Paul Makles
906f15f103 fix: use shape="circle" for friend component 2022-05-30 15:54:55 +01:00
Paul Makles
ea106a3902 chore: bump @revoltchat/ui 2022-05-30 15:48:33 +01:00
Paul Makles
41e533ab59 feat(@ui): port Modal component 2022-05-30 15:45:14 +01:00
Paul Makles
68b9d5ea79 feat(@ui): migrate category / overline and header 2022-05-30 14:42:09 +01:00
Paul Makles
673efc0586 fix: make context menu line divider compact 2022-05-30 12:57:30 +01:00
Paul Makles
74f8c552ed feat(@ui): port category button 2022-05-30 12:56:47 +01:00
Paul Makles
f3bdbe52d9 feat(@ui): migrate textarea and tip 2022-05-30 12:47:13 +01:00
Paul Makles
b4777e9816 feat(@ui): migrate line divider, preloader and save status 2022-05-30 12:40:01 +01:00
Paul Makles
ab77d4a812 feat(@ui): migrate radio 2022-05-30 12:29:56 +01:00
Paul Makles
1d243d4762 feat(@ui): migrate input box 2022-05-30 12:26:16 +01:00
Paul Makles
2f9bfbf83f chore: remove Masks component 2022-05-30 12:19:32 +01:00
Paul Makles
c2547b3ead feat(@ui): migrate icon button 2022-05-30 12:01:47 +01:00
Paul Makles
a64fe61199 feat(@ui): migrate date divider and details 2022-05-29 16:40:02 +01:00
Paul Makles
1bd138d6ef feat(@ui): migrate colour swatches and combo box 2022-05-29 16:38:09 +01:00
Paul Makles
4bcfa601a5 feat(@ui): migrate checkbox component 2022-05-29 16:34:54 +01:00
Paul Makles
20d31babce feat(@ui): migrate Banner component 2022-05-29 15:43:36 +01:00
Paul Makles
12b9716043 fix: open last opened server channel instead of first 2022-05-27 22:32:06 +01:00
Paul Makles
e2d9e41a58 chore: bump revolt.js and @revoltchat/ui 2022-05-27 22:31:12 +01:00
Paul Makles
bdf741e0ee feat: add "ordering" data store 2022-05-27 21:21:42 +01:00
Paul Makles
588cb7c019 feat: finish reimplementation of server list 2022-05-27 21:21:42 +01:00
Paul Makles
94dd4b464a chore: server list integration test 2022-05-27 21:21:42 +01:00
205 changed files with 3724 additions and 4710 deletions

View File

@@ -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"

View File

@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- "master" - "master"
- "production"
jobs: jobs:
to_gitlab: to_gitlab:

View File

@@ -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

View File

@@ -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

View File

@@ -1 +0,0 @@
0.5.3-1

2
external/lang vendored

View File

@@ -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,39 +39,19 @@
"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": {
"@fontsource/bitter": "^4.5.0",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@revoltchat/ui": "^1.0.31",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0",
"sirv-cli": "^1.0.14", "sirv-cli": "^1.0.14",
"vite": "^2.6.14" "vite": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.17.9", "@babel/plugin-proposal-decorators": "^7.17.9",
"@fontsource/atkinson-hyperlegible": "^4.4.5", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bitter": "^4.5.7",
"@fontsource/comic-neue": "^4.4.5", "@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5", "@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5", "@fontsource/inter": "^4.4.5",
@@ -91,13 +71,15 @@
"@fontsource/ubuntu": "^4.4.5", "@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5", "@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0", "@preact/preset-vite": "^2.0.0",
"@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",
"@styled-icons/boxicons-solid": "^10.38.0", "@styled-icons/boxicons-solid": "^10.38.0",
"@styled-icons/simple-icons": "^10.33.0", "@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5", "@tippyjs/react": "4.2.6",
"@traptitech/markdown-it-katex": "^3.4.3", "@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
@@ -107,10 +89,11 @@
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0", "@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13",
"@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",
@@ -122,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",
@@ -132,22 +117,25 @@
"markdown-it-emoji": "^2.0.0", "markdown-it-emoji": "^2.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.6.0", "mobx": "^6.6.0",
"mobx-react-lite": "^3.3.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",
"react-device-detect": "^1.17.0", "qrcode.react": "^3.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-device-detect": "2.2.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-hook-form": "6.3.0", "react-hook-form": "6.3.0",
"react-overlapping-panels": "1.2.2", "react-overlapping-panels": "1.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"react-virtuoso": "^1.10.4", "react-virtuoso": "^2.12.0",
"revolt.js": "6.0.0-2", "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
View 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;

View File

@@ -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
View 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`

View File

@@ -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" />

View File

@@ -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,

View File

@@ -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>

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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)}>

View File

@@ -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

View File

@@ -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,
) )
} }

View File

@@ -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;
} }

View 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;

View File

@@ -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;
} }

View 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>
); );
} }

View File

@@ -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={

View File

@@ -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>
)}
</> </>
); );
} }

View File

@@ -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>

View File

@@ -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>
}
/>
); );
} }

View File

@@ -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" />}>

View File

@@ -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";

View File

@@ -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>

View File

@@ -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})`;
} }

View File

@@ -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"

View File

@@ -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>

View File

@@ -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" && (

View File

@@ -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();

View File

@@ -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" />
)} )}

View File

@@ -1,448 +1,42 @@
import { Plus } from "@styled-icons/boxicons-regular";
import { Cog, Compass } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useHistory, useLocation, useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import styled, { css } from "styled-components/macro";
import { useTriggerEvents } from "preact-context-menu"; import { useCallback } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink"; import { ServerList } from "@revoltchat/ui";
import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { SIDEBAR_CHANNELS } from "../../../mobx/stores/Layout";
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 ChannelIcon from "../../common/ChannelIcon"; /**
import ServerIcon from "../../common/ServerIcon"; * Server list sidebar shim component
import Tooltip from "../../common/Tooltip"; */
import UserHover from "../../common/user/UserHover";
import UserIcon from "../../common/user/UserIcon";
import IconButton from "../../ui/IconButton";
import LineDivider from "../../ui/LineDivider";
import { Children } from "../../../types/Preact";
function Icon({
children,
unread,
count,
size,
}: {
children: Children;
unread?: "mention" | "unread";
count: number | 0;
size: number;
}) {
return (
<svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32">
<use href="#serverIndicator" />
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={unread ? "url(#server)" : undefined}>
{children}
</foreignObject>
{unread === "unread" && (
<circle cx="27" cy="5" r="5" fill={"white"} />
)}
{unread === "mention" && (
<>
<circle cx="27" cy="5" r="5" fill={"var(--error)"} />
<text
x="27"
y="5"
r="5"
fill={"white"}
fontSize={"7.5"}
fontWeight={600}
text-anchor="middle"
alignmentBaseline={"middle"}
dominant-baseline={"middle"}>
{count < 10 ? count : "9+"}
</text>
</>
)}
</svg>
);
}
const ServersBase = styled.div`
width: 58px;
height: 100%;
padding-inline-start: 2px;
display: flex;
flex-shrink: 0;
flex-direction: column;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`;
const ServerList = styled.div`
flex-grow: 1;
display: flex;
overflow-y: scroll;
padding-bottom: 20px;
flex-direction: column;
scrollbar-width: none;
> :first-child > svg {
margin: 6px 0 6px 4px;
}
&::-webkit-scrollbar {
width: 0px;
}
`;
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
height: 54px;
display: flex;
align-items: center;
//transition: 0.2s ease height;
:focus {
outline: 3px solid blue;
}
> div {
height: 42px;
padding-inline-start: 6px;
display: grid;
place-items: center;
border-start-start-radius: 50%;
border-end-start-radius: 50%;
&:active {
transform: translateY(1px);
}
${(props) =>
props.active &&
css`
&:active {
transform: none;
}
`}
}
> span {
width: 0;
display: relative;
${(props) =>
!props.active &&
css`
display: none;
`}
svg {
margin-top: 5px;
pointer-events: none;
}
}
${(props) =>
(!props.active || props.home) &&
css`
cursor: pointer;
`}
`;
const ServerCircle = styled.div`
width: 54px;
height: 54px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.circle {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-background);
border-radius: 50%;
height: 42px;
width: 42px;
transition: background-color 0.1s ease-in;
cursor: pointer;
> div svg {
color: var(--accent);
}
&:active {
transform: translateY(1px);
}
}
`;
const SettingsButton = styled.div`
width: 50px;
height: 56px;
display: grid;
place-items: center;
`;
function Swoosh() {
const sidebarOpen = useApplicationState().layout.getSectionState(
SIDEBAR_CHANNELS,
true,
);
const fill = sidebarOpen
? "var(--sidebar-active)"
: "var(--primary-background)";
return (
<span>
<svg
width="56"
height="106"
viewBox="0 0 56 106"
xmlns="http://www.w3.org/2000/svg">
<path
d="M54 53C54 67.9117 41.9117 80 27 80C12.0883 80 0 67.9117 0 53C0 38.0883 12.0883 26 27 26C41.9117 26 54 38.0883 54 53Z"
fill={fill}
/>
<path
d="M27.0002 80C4.50023 80 56.0002 53 56.0002 53V106C56.0002 106 49.5002 80 27.0002 80Z"
fill={fill}
/>
<path
d="M27.0003 26C4.50025 26 56 53 56 53L56.0003 0C56.0003 0 49.5003 26 27.0003 26Z"
fill={fill}
/>
<rect x="51" y="50" width="5" height="7" fill={fill} />
</svg>
</span>
);
}
export default observer(() => { export default observer(() => {
const client = useClient(); const client = useClient();
const state = useApplicationState(); const state = useApplicationState();
const { server: server_id } = useParams<{ server?: string }>();
const server = server_id ? client.servers.get(server_id) : undefined;
const servers = [...client.servers.values()];
const channels = [...client.channels.values()];
const history = useHistory();
const path = useLocation().pathname;
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const { server: server_id } = useParams<{ server?: string }>();
let alertCount = [...client.users.values()].filter( const createServer = useCallback(
(x) => x.relationship === "Incoming", () =>
).length;
const homeActive =
typeof server === "undefined" &&
!path.startsWith("/invite") &&
!path.startsWith("/discover");
return (
<ServersBase>
<ServerList>
<ConditionalLink
active={homeActive}
to={state.layout.getLastHomePath()}>
<ServerEntry home active={homeActive}>
<Swoosh />
<div
{...useTriggerEvents("Status")}
onClick={() =>
homeActive && history.push("/settings")
}>
<UserHover user={client.user ?? undefined}>
<Icon
size={42}
unread={
alertCount > 0 ? "mention" : undefined
}
count={alertCount}>
<UserIcon
target={client.user ?? undefined}
size={32}
status
hover
/>
</Icon>
</UserHover>
</div>
</ServerEntry>
</ConditionalLink>
{channels
.filter(
(x) =>
((x.channel_type === "DirectMessage" && x.active) ||
x.channel_type === "Group") &&
x.unread,
)
.map((x) => {
const unreadCount = x.mentions.length;
return (
<Link to={`/channel/${x._id}`}>
<ServerEntry
home
active={false}
{...useTriggerEvents("Menu", {
channel: x._id,
unread: true,
})}>
<div>
<Icon
size={42}
unread={
unreadCount > 0
? "mention"
: "unread"
}
count={unreadCount}>
{x.channel_type ===
"DirectMessage" ? (
<UserIcon
target={x.recipient}
size={32}
hover
/>
) : (
<ChannelIcon
target={x}
size={32}
hover
/>
)}
</Icon>
</div>
</ServerEntry>
</Link>
);
})}
<LineDivider />
{servers.map((server) => {
const active = server._id === server_id;
const isUnread = server.isUnread(state.notifications);
const mentionCount = server.getMentions(
state.notifications,
).length;
return (
<ConditionalLink
key={server._id}
active={active}
to={state.layout.getServerPath(server._id)}>
<ServerEntry
active={active}
{...useTriggerEvents("Menu", {
server: server._id,
unread: isUnread,
})}>
<Swoosh />
<Tooltip
content={server.name}
placement="right">
<Icon
size={42}
unread={
mentionCount > 0
? "mention"
: isUnread
? "unread"
: undefined
}
count={mentionCount}>
<ServerIcon size={32} target={server} />
</Icon>
</Tooltip>
</ServerEntry>
</ConditionalLink>
);
})}
{/*<LineDivider />*/}
<ServerCircle>
<Tooltip content="Add a Server" placement="right">
<div className="circle">
<IconButton
onClick={() =>
openScreen({ openScreen({
id: "special_input", id: "special_input",
type: "create_server", type: "create_server",
}) }),
}> [],
<Plus size={32} /> );
</IconButton>
</div>
</Tooltip>
</ServerCircle>
{!isTouchscreenDevice && (
<ServerCircle>
<Tooltip
content={
<div
style={{
display: "flex",
alignItems: "center",
gap: "6px",
}}>
<div>Discover Revolt</div>
<div
style={{
padding: "1px 5px",
fontSize: "9px",
background: "var(--status-busy)",
borderRadius: "60px",
}}>
NEW
</div>
</div>
}
placement="right">
<div className="circle">
<IconButton>
<Link to="/discover">
<a>
<Compass size={32} />
</a>
</Link>
</IconButton>
</div>
</Tooltip>
</ServerCircle>
)}
</ServerList>
{!isTouchscreenDevice && ( return (
<Tooltip content={"Settings"} placement="right"> <ServerList
<ServerCircle> client={client}
<Link to="/settings"> active={server_id}
<div className="circle"> createServer={createServer}
<IconButton> permit={state.notifications}
<Cog home={state.layout.getLastHomePath}
size={24} servers={state.ordering.orderedServers}
fill="var(--secondary-foreground) !important" reorder={state.ordering.reorderServer}
/> />
</IconButton>
</div>
</Link>
</ServerCircle>
</Tooltip>
)}
<PaintCounter small />
</ServersBase>
); );
}); });

View File

@@ -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>,
); );

View File

@@ -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.

View File

@@ -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();

View File

@@ -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

View 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>
</>
);
}

View 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>
))}
</>
);
});

View 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>
)}
</>
);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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({

View File

@@ -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" />}>
{" "} {" "}

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -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)

View File

@@ -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);
`;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
`;

View File

@@ -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>
);
}

View File

@@ -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);
}
}
`;

View File

@@ -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 />}

View File

@@ -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});
}
`}
`;

View File

@@ -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);
}
`}
`;

View File

@@ -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);
`;

View File

@@ -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>
);
}

View File

@@ -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,
);
}

View File

@@ -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 && <> &middot; </>}
{props.error && (
<Overline type="error">
<Text id={`error.${props.error}`}>{props.error}</Text>
</Overline>
)}
</OverlineBase>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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);
`;

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import { createBrowserHistory } from "history";
export const history = createBrowserHistory({
basename: import.meta.env.BASE_URL,
});

View File

@@ -1,18 +1,28 @@
import { BrowserRouter as Router } from "react-router-dom"; import { Router, Link } from "react-router-dom";
import { ContextMenuTrigger } from "preact-context-menu";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
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
@@ -27,7 +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}>
<UIProvider value={uiContext}>
<Locale> <Locale>
<Intermediate> <Intermediate>
<Client> <Client>
@@ -35,7 +46,9 @@ export default function Context({ children }: { children: Children }) {
<SyncManager /> <SyncManager />
</Client> </Client>
</Intermediate> </Intermediate>
<ModalRenderer />
</Locale> </Locale>
</UIProvider>
<Theme /> <Theme />
</Router> </Router>
); );

View File

@@ -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
}, []); }, []);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)
} }
/> />

View File

@@ -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";

View File

@@ -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)}

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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> &middot; <I18nError error={error} />
</Category>
)} )}
</form> </form>
</Modal> </Modal>

View File

@@ -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 && (
<> <>

View File

@@ -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>
);
});

View File

@@ -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;
}
}

View File

@@ -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>
);
});

View File

@@ -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;
}
}
}
} }

View File

@@ -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]);

View File

@@ -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>
); );
}, },

View 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;
});

View 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>
);
}

View File

@@ -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>
); );
} }

Some files were not shown because too many files have changed in this diff Show More