Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (217)
Showing
with 521 additions and 254 deletions
...@@ -3,3 +3,4 @@ node_modules ...@@ -3,3 +3,4 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
*.log
image: node:14-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
module.exports = { module.exports = {
"tabWidth": 4, tabWidth: 4,
"trailingComma": "all", trailingComma: "all",
"jsxBracketSameLine": true, jsxBracketSameLine: true,
"importOrder": ["preact|classnames|.scss$", "/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"], importOrder: [
"importOrderSeparation": true, "preact|classnames|.scss$",
} "/(lib)",
"/(redux|mobx)",
\ No newline at end of file "/(context)",
"/(ui|common)|.svg$",
"^[./]",
],
importOrderSeparation: true,
};
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
\ No newline at end of file
...@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least ...@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
Revite Revite
Copyright (C) 2021 REVOLT Copyright (C) 2021 Revolt Communications
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published it under the terms of the GNU Affero General Public License as published
......
Subproject commit 2a9ef2c8025dd71a20fddca7c94cb7af30978c4b Subproject commit 09955e9d30c19c1a180fd3aacdb85961641da2bc
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>REVOLT</title> <title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Revolt"> <meta name="apple-mobile-web-app-title" content="Revolt" />
<!--<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />--> <meta
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> name="viewport"
<meta name="apple-mobile-web-app-capable" content="yes"> content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<!--App Icons--> <!--App Icons-->
<link rel="apple-touch-icon" href="public/assets/icons/apple-touch.png"> <link
<link rel="icon" type="image/png" href="/src/assets/logo_round.png" /> rel="apple-touch-icon"
href="public/assets/icons/apple-touch.png"
/>
<link rel="icon" type="image/png" href="/src/assets/logo_round.png" />
<!--Splash Screens for iOS Devices--> <!--Splash Screens for iOS Devices-->
<link href="public/assets/splashscreens/iphone5_splash.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link
<link href="public/assets/splashscreens/iphone6_splash.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> href="public/assets/splashscreens/iphone5_splash.png"
<link href="public/assets/splashscreens/iphoneplus_splash.png" media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
<link href="public/assets/splashscreens/iphonex_splash.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> rel="apple-touch-startup-image"
<link href="public/assets/splashscreens/iphonexr_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> />
<link href="public/assets/splashscreens/iphonexsmax_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> <link
<link href="public/assets/splashscreens/ipad_splash.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> href="public/assets/splashscreens/iphone6_splash.png"
<link href="public/assets/splashscreens/ipadpro1_splash.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
<link href="public/assets/splashscreens/ipadpro3_splash.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> rel="apple-touch-startup-image"
<link href="public/assets/splashscreens/ipadpro2_splash.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> />
</head> <link
<body> href="public/assets/splashscreens/iphoneplus_splash.png"
<div id="app"></div> media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)"
<script type="module" src="/src/main.tsx"></script> rel="apple-touch-startup-image"
</body> />
<style> <link
html { href="public/assets/splashscreens/iphonex_splash.png"
background-color: #191919; media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)"
} rel="apple-touch-startup-image"
</style> />
</html> <link
\ No newline at end of file href="public/assets/splashscreens/iphonexr_splash.png"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphonexsmax_splash.png"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipad_splash.png"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro1_splash.png"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro3_splash.png"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro2_splash.png"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
</head>
<body ontouchstart="">
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<style>
html {
background-color: #191919;
}
</style>
</html>
{ {
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "rimraf build && vite build", "build": "rimraf build && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"eslintConfig": { "eslintConfig": {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [
"preact", "preact",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"ignorePatterns": [ "ignorePatterns": [
"build/" "build/"
], ],
"rules": { "rules": {
"@typescript-eslint/explicit-module-boundary-types": "off" "radix": "off",
} "no-spaced-func": "off",
}, "react/no-danger": "off",
"dependencies": { "@typescript-eslint/explicit-module-boundary-types": "off",
"preact": "^10.5.13" "@typescript-eslint/no-non-null-assertion": "off",
}, "@typescript-eslint/no-unused-vars": [
"devDependencies": { "warn",
"@fontsource/atkinson-hyperlegible": "^4.4.5", {
"@fontsource/bree-serif": "^4.4.5", "varsIgnorePattern": "^_"
"@fontsource/comic-neue": "^4.4.5", }
"@fontsource/fira-code": "^4.4.5", ],
"@fontsource/inter": "^4.4.5", "no-unused-vars": [
"@fontsource/lato": "^4.4.5", "warn",
"@fontsource/montserrat": "^4.4.5", {
"@fontsource/noto-sans": "^4.4.5", "varsIgnorePattern": "^_"
"@fontsource/open-sans": "^4.4.5", }
"@fontsource/poppins": "^4.4.5", ]
"@fontsource/raleway": "^4.4.5", }
"@fontsource/roboto": "^4.4.5", },
"@fontsource/roboto-mono": "^4.4.5", "dependencies": {
"@fontsource/source-code-pro": "^4.4.5", "vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
"@fontsource/space-mono": "^4.4.5", },
"@fontsource/ubuntu": "^4.4.5", "devDependencies": {
"@fontsource/ubuntu-mono": "^4.4.5", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6", "@fontsource/bree-serif": "^4.4.5",
"@preact/preset-vite": "^2.0.0", "@fontsource/comic-neue": "^4.4.5",
"@rollup/plugin-replace": "^2.4.2", "@fontsource/fira-code": "^4.4.5",
"@styled-icons/boxicons-logos": "^10.34.0", "@fontsource/inter": "^4.4.5",
"@styled-icons/boxicons-regular": "^10.34.0", "@fontsource/lato": "^4.4.5",
"@styled-icons/boxicons-solid": "^10.34.0", "@fontsource/montserrat": "^4.4.5",
"@styled-icons/simple-icons": "^10.33.0", "@fontsource/noto-sans": "^4.4.5",
"@tippyjs/react": "^4.2.5", "@fontsource/open-sans": "^4.4.5",
"@traptitech/markdown-it-katex": "^3.4.3", "@fontsource/poppins": "^4.4.5",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@fontsource/raleway": "^4.4.5",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@fontsource/roboto": "^4.4.5",
"@types/lodash.defaultsdeep": "^4.6.6", "@fontsource/roboto-mono": "^4.4.5",
"@types/lodash.isequal": "^4.5.5", "@fontsource/source-code-pro": "^4.4.5",
"@types/markdown-it": "^12.0.2", "@fontsource/space-mono": "^4.4.5",
"@types/node": "^15.12.4", "@fontsource/ubuntu": "^4.4.5",
"@types/preact-i18n": "^2.3.0", "@fontsource/ubuntu-mono": "^4.4.5",
"@types/prismjs": "^1.16.5", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@types/react-helmet": "^6.1.1", "@preact/preset-vite": "^2.0.0",
"@types/react-router-dom": "^5.1.7", "@rollup/plugin-replace": "^2.4.2",
"@types/react-scroll": "^1.8.2", "@styled-icons/boxicons-logos": "^10.34.0",
"@types/styled-components": "^5.1.10", "@styled-icons/boxicons-regular": "^10.34.0",
"@types/twemoji": "^12.1.1", "@styled-icons/boxicons-solid": "^10.37.0",
"@typescript-eslint/eslint-plugin": "^4.27.0", "@styled-icons/simple-icons": "^10.33.0",
"@typescript-eslint/parser": "^4.27.0", "@tippyjs/react": "^4.2.5",
"classnames": "^2.3.1", "@traptitech/markdown-it-katex": "^3.4.3",
"dayjs": "^1.10.5", "@traptitech/markdown-it-spoiler": "^1.1.6",
"detect-browser": "^5.2.0", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
"eslint": "^7.28.0", "@types/lodash.defaultsdeep": "^4.6.6",
"eslint-config-preact": "^1.1.4", "@types/lodash.isequal": "^4.5.5",
"eventemitter3": "^4.0.7", "@types/markdown-it": "^12.0.2",
"highlight.js": "^11.0.1", "@types/node": "^15.12.4",
"idb": "^6.1.2", "@types/preact-i18n": "^2.3.0",
"localforage": "^1.9.0", "@types/prismjs": "^1.16.5",
"lodash.defaultsdeep": "^4.6.1", "@types/react-helmet": "^6.1.1",
"lodash.isequal": "^4.5.0", "@types/react-router-dom": "^5.1.7",
"markdown-it": "^12.0.6", "@types/react-scroll": "^1.8.2",
"markdown-it-emoji": "^2.0.0", "@types/styled-components": "^5.1.10",
"markdown-it-sub": "^1.0.0", "@types/twemoji": "^12.1.1",
"markdown-it-sup": "^1.0.0", "@typescript-eslint/eslint-plugin": "^4.27.0",
"mediasoup-client": "^3.6.33", "@typescript-eslint/parser": "^4.27.0",
"preact-context-menu": "^0.1.5", "classnames": "^2.3.1",
"preact-i18n": "^2.4.0-preactx", "dayjs": "^1.10.6",
"prettier": "^2.3.1", "detect-browser": "^5.2.0",
"prismjs": "^1.23.0", "eslint": "^7.28.0",
"react-device-detect": "^1.17.0", "eslint-config-preact": "^1.1.4",
"react-helmet": "^6.1.0", "eventemitter3": "^4.0.7",
"react-hook-form": "6.3.0", "highlight.js": "^11.0.1",
"react-overlapping-panels": "1.2.1", "localforage": "^1.9.0",
"react-redux": "^7.2.4", "lodash.defaultsdeep": "^4.6.1",
"react-router-dom": "^5.2.0", "lodash.isequal": "^4.5.0",
"react-scroll": "^1.8.2", "markdown-it": "^12.0.6",
"redux": "^4.1.0", "markdown-it-emoji": "^2.0.0",
"revolt.js": "4.3.3-alpha.9", "markdown-it-sub": "^1.0.0",
"rimraf": "^3.0.2", "markdown-it-sup": "^1.0.0",
"sass": "^1.35.1", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"shade-blend-color": "^1.0.0", "mobx": "^6.3.2",
"styled-components": "^5.3.0", "mobx-react-lite": "^3.2.0",
"typescript": "^4.3.2", "preact": "^10.5.14",
"ulid": "^2.3.0", "preact-context-menu": "^0.1.5",
"use-resize-observer": "^7.0.0", "preact-i18n": "^2.4.0-preactx",
"vite": "npm:@insertish/vite@2.2.4-dynamic-import-css-f428476", "prettier": "^2.3.1",
"vite-plugin-pwa": "^0.8.1", "prismjs": "^1.23.0",
"workbox-precaching": "^6.1.5" "react-device-detect": "^1.17.0",
} "react-helmet": "^6.1.0",
"react-hook-form": "6.3.0",
"react-overlapping-panels": "1.2.2",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2",
"redux": "^4.1.0",
"revolt-api": "0.5.1-alpha.10-patch.0",
"revolt.js": "5.0.0-alpha.18",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
"shade-blend-color": "^1.0.0",
"styled-components": "^5.3.0",
"typescript": "^4.3.2",
"ulid": "^2.3.0",
"use-resize-observer": "^7.0.0",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5"
},
"name": "client",
"main": "index.js",
"repository": "https://gitlab.insrt.uk/revolt/revite.git",
"author": "Paul <paulmakles@gmail.com>",
"license": "MIT"
} }
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "chat.revolt.app.twa",
"sha256_cert_fingerprints": [
"6E:62:C1:BF:5A:2D:11:31:A3:22:91:8D:22:2B:2C:49:D3:70:F3:A1:45:DF:11:6A:97:DC:4C:A9:3B:C3:AA:FB"
]
}
}]
\ No newline at end of file
public/assets/icons/masking-512x512.png

4.66 KiB

<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.9299 18.0767C30.9299 20.7484 29.4776 22.3521 26.3783 22.3521H21.2468V13.8981H26.3792C29.4776 13.8981 30.9299 15.5498 30.9299 18.0767ZM8 7L12.9681 13.9144V41.1006H21.2477V28.2293H23.2331L30.3031 41.1035H39.648L31.8041 27.5976C33.9941 27.0629 35.9365 25.7938 37.3097 24.0006C38.683 22.2073 39.4048 19.9973 39.3556 17.7364C39.3556 11.8093 35.192 7 26.8636 7H8Z" fill="white"/>
</svg>
export const emojiDictionary = { export const emojiDictionary = {
"100": "💯", 100: "💯",
"1234": "🔢", 1234: "🔢",
grinning: "😀", grinning: "😀",
smiley: "😃", smiley: "😃",
smile: "😄", smile: "😄",
......
...@@ -19,8 +19,8 @@ export const SOUNDS_ARRAY: Sounds[] = [ ...@@ -19,8 +19,8 @@ export const SOUNDS_ARRAY: Sounds[] = [
]; ];
export function playSound(sound: Sounds) { export function playSound(sound: Sounds) {
let file = SoundMap[sound]; const file = SoundMap[sound];
let el = new Audio(file); const el = new Audio(file);
try { try {
el.play(); el.play();
} catch (err) { } catch (err) {
......
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useState } from "preact/hooks";
import { dispatch, getState } from "../../redux";
import Button from "../ui/Button";
import Checkbox from "../ui/Checkbox";
import { Children } from "../../types/Preact";
const Base = styled.div`
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
padding: 12px;
img {
height: 150px;
}
.subtext {
color: var(--secondary-foreground);
margin-bottom: 12px;
font-size: 14px;
}
.actions {
margin-top: 20px;
display: flex;
gap: 12px;
}
`;
type Props = {
gated: boolean;
children: Children;
} & {
type: "channel";
channel: Channel;
};
export default observer((props: Props) => {
const history = useHistory();
const [consent, setConsent] = useState(
getState().sectionToggle["nsfw"] ?? false,
);
const [ageGate, setAgeGate] = useState(false);
if (ageGate || !props.gated) {
return <>{props.children}</>;
}
if (
!(
props.channel.channel_type === "Group" ||
props.channel.channel_type === "TextChannel"
)
)
return <>{props.children}</>;
return (
<Base>
<img
loading="eager"
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
draggable={false}
/>
<h2>{props.channel.name}</h2>
<span className="subtext">
<Text id={`app.main.channel.nsfw.${props.type}.marked`} />{" "}
<a href="#">
<Text id={`app.main.channel.nsfw.learn_more`} />
</a>
</span>
<Checkbox
checked={consent}
onChange={(v) => {
setConsent(v);
if (v) {
dispatch({
type: "SECTION_TOGGLE_SET",
id: "nsfw",
state: true,
});
} else {
dispatch({ type: "SECTION_TOGGLE_UNSET", id: "nsfw" });
}
}}>
<Text id="app.main.channel.nsfw.confirm" />
</Checkbox>
<div className="actions">
<Button contrast onClick={() => history.goBack()}>
<Text id="app.special.modals.actions.back" />
</Button>
<Button contrast onClick={() => consent && setAgeGate(true)}>
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
</Button>
</div>
</Base>
);
});
import { SYSTEM_USER_ID, User } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import { Channels } from "revolt.js/dist/api/objects"; import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { StateUpdater, useContext, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { useClient } from "../../context/revoltjs/RevoltClient";
import { emojiDictionary } from "../../assets/emojis"; import { emojiDictionary } from "../../assets/emojis";
import ChannelIcon from "./ChannelIcon"; import ChannelIcon from "./ChannelIcon";
...@@ -24,7 +25,7 @@ export type AutoCompleteState = ...@@ -24,7 +25,7 @@ export type AutoCompleteState =
} }
| { | {
type: "channel"; type: "channel";
matches: Channels.TextChannel[]; matches: Channel[];
} }
)); ));
...@@ -52,16 +53,16 @@ export function useAutoComplete( ...@@ -52,16 +53,16 @@ export function useAutoComplete(
): AutoCompleteProps { ): AutoCompleteProps {
const [state, setState] = useState<AutoCompleteState>({ type: "none" }); const [state, setState] = useState<AutoCompleteState>({ type: "none" });
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const client = useContext(AppContext); const client = useClient();
function findSearchString( function findSearchString(
el: HTMLTextAreaElement, el: HTMLTextAreaElement,
): ["emoji" | "user" | "channel", string, number] | undefined { ): ["emoji" | "user" | "channel", string, number] | undefined {
if (el.selectionStart === el.selectionEnd) { if (el.selectionStart === el.selectionEnd) {
let cursor = el.selectionStart; const cursor = el.selectionStart;
let content = el.value.slice(0, cursor); const content = el.value.slice(0, cursor);
let valid = /\w/; const valid = /\w/;
let j = content.length - 1; let j = content.length - 1;
if (content[j] === "@") { if (content[j] === "@") {
...@@ -75,10 +76,10 @@ export function useAutoComplete( ...@@ -75,10 +76,10 @@ export function useAutoComplete(
} }
if (j === -1) return; if (j === -1) return;
let current = content[j]; const current = content[j];
if (current === ":" || current === "@" || current === "#") { if (current === ":" || current === "@" || current === "#") {
let search = content.slice(j + 1, content.length); const search = content.slice(j + 1, content.length);
if (search.length > 0) { if (search.length > 0) {
return [ return [
current === "#" current === "#"
...@@ -97,19 +98,19 @@ export function useAutoComplete( ...@@ -97,19 +98,19 @@ export function useAutoComplete(
function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) { function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
const el = ev.currentTarget; const el = ev.currentTarget;
let result = findSearchString(el); const result = findSearchString(el);
if (result) { if (result) {
let [type, search] = result; const [type, search] = result;
const regex = new RegExp(search, "i"); const regex = new RegExp(search, "i");
if (type === "emoji") { if (type === "emoji") {
// ! FIXME: we should convert it to a Binary Search Tree and use that // ! TODO: we should convert it to a Binary Search Tree and use that
let matches = Object.keys(emojiDictionary) const matches = Object.keys(emojiDictionary)
.filter((emoji: string) => emoji.match(regex)) .filter((emoji: string) => emoji.match(regex))
.splice(0, 5); .splice(0, 5);
if (matches.length > 0) { if (matches.length > 0) {
let currentPosition = const currentPosition =
state.type !== "none" ? state.selected : 0; state.type !== "none" ? state.selected : 0;
setState({ setState({
...@@ -127,32 +128,30 @@ export function useAutoComplete( ...@@ -127,32 +128,30 @@ export function useAutoComplete(
let users: User[] = []; let users: User[] = [];
switch (searchClues.users.type) { switch (searchClues.users.type) {
case "all": case "all":
users = client.users.toArray(); users = [...client.users.values()];
break; break;
case "channel": { case "channel": {
let channel = client.channels.get(searchClues.users.id); const channel = client.channels.get(
searchClues.users.id,
);
switch (channel?.channel_type) { switch (channel?.channel_type) {
case "Group": case "Group":
case "DirectMessage": case "DirectMessage":
users = client.users users = channel.recipients!.filter(
.mapKeys(channel.recipients) (x) => typeof x !== "undefined",
.filter( ) as User[];
(x) => typeof x !== "undefined",
) as User[];
break; break;
case "TextChannel": case "TextChannel":
const server = channel.server; {
users = client.servers.members const server = channel.server_id;
.toArray() users = [...client.members.keys()]
.filter( .map((x) => JSON.parse(x))
(x) => x._id.substr(0, 26) === server, .filter((x) => x.server === server)
) .map((x) => client.users.get(x.user))
.map((x) => .filter(
client.users.get(x._id.substr(26)), (x) => typeof x !== "undefined",
) ) as User[];
.filter( }
(x) => typeof x !== "undefined",
) as User[];
break; break;
default: default:
return; return;
...@@ -162,7 +161,7 @@ export function useAutoComplete( ...@@ -162,7 +161,7 @@ export function useAutoComplete(
users = users.filter((x) => x._id !== SYSTEM_USER_ID); users = users.filter((x) => x._id !== SYSTEM_USER_ID);
let matches = ( const matches = (
search.length > 0 search.length > 0
? users.filter((user) => ? users.filter((user) =>
user.username.toLowerCase().match(regex), user.username.toLowerCase().match(regex),
...@@ -173,7 +172,7 @@ export function useAutoComplete( ...@@ -173,7 +172,7 @@ export function useAutoComplete(
.filter((x) => typeof x !== "undefined"); .filter((x) => typeof x !== "undefined");
if (matches.length > 0) { if (matches.length > 0) {
let currentPosition = const currentPosition =
state.type !== "none" ? state.selected : 0; state.type !== "none" ? state.selected : 0;
setState({ setState({
...@@ -188,17 +187,16 @@ export function useAutoComplete( ...@@ -188,17 +187,16 @@ export function useAutoComplete(
} }
if (type === "channel" && searchClues?.channels) { if (type === "channel" && searchClues?.channels) {
let channels = client.servers const channels = client.servers
.get(searchClues.channels.server) .get(searchClues.channels.server)
?.channels.map((x) => client.channels.get(x)) ?.channels.filter(
.filter(
(x) => typeof x !== "undefined", (x) => typeof x !== "undefined",
) as Channels.TextChannel[]; ) as Channel[];
let matches = ( const matches = (
search.length > 0 search.length > 0
? channels.filter((channel) => ? channels.filter((channel) =>
channel.name.toLowerCase().match(regex), channel.name!.toLowerCase().match(regex),
) )
: channels : channels
) )
...@@ -206,7 +204,7 @@ export function useAutoComplete( ...@@ -206,7 +204,7 @@ export function useAutoComplete(
.filter((x) => typeof x !== "undefined"); .filter((x) => typeof x !== "undefined");
if (matches.length > 0) { if (matches.length > 0) {
let currentPosition = const currentPosition =
state.type !== "none" ? state.selected : 0; state.type !== "none" ? state.selected : 0;
setState({ setState({
...@@ -228,11 +226,11 @@ export function useAutoComplete( ...@@ -228,11 +226,11 @@ export function useAutoComplete(
function selectCurrent(el: HTMLTextAreaElement) { function selectCurrent(el: HTMLTextAreaElement) {
if (state.type !== "none") { if (state.type !== "none") {
let result = findSearchString(el); const result = findSearchString(el);
if (result) { if (result) {
let [_type, search, index] = result; const [_type, search, index] = result;
let content = el.value.split(""); const content = el.value.split("");
if (state.type === "emoji") { if (state.type === "emoji") {
content.splice( content.splice(
index, index,
...@@ -307,7 +305,7 @@ export function useAutoComplete( ...@@ -307,7 +305,7 @@ export function useAutoComplete(
function onKeyUp(e: KeyboardEvent) { function onKeyUp(e: KeyboardEvent) {
if (e.currentTarget !== null) { if (e.currentTarget !== null) {
// @ts-expect-error // @ts-expect-error Type mis-match.
onChange(e); onChange(e);
} }
} }
...@@ -353,12 +351,13 @@ const Base = styled.div<{ detached?: boolean }>` ...@@ -353,12 +351,13 @@ const Base = styled.div<{ detached?: boolean }>`
display: flex; display: flex;
font-size: 1em; font-size: 1em;
cursor: pointer; cursor: pointer;
border-radius: 6px;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
font-family: inherit;
background: transparent; background: transparent;
color: var(--foreground); color: var(--foreground);
width: calc(100% - 12px); width: calc(100% - 12px);
border-radius: var(--border-radius);
span { span {
display: grid; display: grid;
...@@ -376,7 +375,7 @@ const Base = styled.div<{ detached?: boolean }>` ...@@ -376,7 +375,7 @@ const Base = styled.div<{ detached?: boolean }>`
bottom: 8px; bottom: 8px;
> div { > div {
border-radius: 4px; border-radius: var(--border-radius);
} }
`} `}
`; `;
...@@ -393,6 +392,7 @@ export default function AutoComplete({ ...@@ -393,6 +392,7 @@ export default function AutoComplete({
{state.type === "emoji" && {state.type === "emoji" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
...@@ -424,6 +424,7 @@ export default function AutoComplete({ ...@@ -424,6 +424,7 @@ export default function AutoComplete({
{state.type === "user" && {state.type === "user" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
...@@ -448,6 +449,7 @@ export default function AutoComplete({ ...@@ -448,6 +449,7 @@ export default function AutoComplete({
{state.type === "channel" && {state.type === "channel" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
......
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular"; import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
import { Channels } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
...@@ -8,58 +9,59 @@ import { AppContext } from "../../context/revoltjs/RevoltClient"; ...@@ -8,58 +9,59 @@ import { AppContext } from "../../context/revoltjs/RevoltClient";
import { ImageIconBase, IconBaseProps } from "./IconBase"; import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png"; import fallback from "./assets/group.png";
interface Props interface Props extends IconBaseProps<Channel> {
extends IconBaseProps<
Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel
> {
isServerChannel?: boolean; isServerChannel?: boolean;
} }
export default function ChannelIcon( export default observer(
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>, (
) { props: Props &
const client = useContext(AppContext); Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext);
const { const {
size, size,
target, target,
attachment, attachment,
isServerChannel: server, isServerChannel: server,
animate, animate,
children, ...imgProps
as, } = props;
...imgProps const iconURL = client.generateFileURL(
} = props; target?.icon ?? attachment,
const iconURL = client.generateFileURL( { max_side: 256 },
target?.icon ?? attachment, animate,
{ max_side: 256 }, );
animate, const isServerChannel =
); server ||
const isServerChannel = (target &&
server || (target.channel_type === "TextChannel" ||
(target && target.channel_type === "VoiceChannel"));
(target.channel_type === "TextChannel" ||
target.channel_type === "VoiceChannel"));
if (typeof iconURL === "undefined") { if (typeof iconURL === "undefined") {
if (isServerChannel) { if (isServerChannel) {
if (target?.channel_type === "VoiceChannel") { if (target?.channel_type === "VoiceChannel") {
return <VolumeFull size={size} />; return <VolumeFull size={size} />;
} else { }
return <Hash size={size} />; return <Hash size={size} />;
} }
} }
}
return ( return (
// ! fixme: replace fallback with <picture /> + <source /> // ! TODO: replace fallback with <picture /> + <source />
<ImageIconBase <ImageIconBase
{...imgProps} {...imgProps}
width={size} width={size}
height={size} height={size}
aria-hidden="true" loading="lazy"
square={isServerChannel} aria-hidden="true"
src={iconURL ?? fallback} square={isServerChannel}
/> src={iconURL ?? fallback}
); />
} );
},
);
import { EmojiPacks } from "../../redux/reducers/settings"; import { EmojiPacks } from "../../redux/reducers/settings";
var EMOJI_PACK = "mutant"; let EMOJI_PACK = "mutant";
const REVISION = 3; const REVISION = 3;
export function setEmojiPack(pack: EmojiPacks) { export function setEmojiPack(pack: EmojiPacks) {
...@@ -41,7 +41,7 @@ function toCodePoint(rune: string) { ...@@ -41,7 +41,7 @@ function toCodePoint(rune: string) {
} }
function parseEmoji(emoji: string) { function parseEmoji(emoji: string) {
let codepoint = toCodePoint(emoji); const codepoint = toCodePoint(emoji);
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`; return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
} }
...@@ -55,6 +55,7 @@ export default function Emoji({ ...@@ -55,6 +55,7 @@ export default function Emoji({
return ( return (
<img <img
alt={emoji} alt={emoji}
loading="lazy"
className="emoji" className="emoji"
draggable={false} draggable={false}
src={parseEmoji(emoji)} src={parseEmoji(emoji)}
...@@ -66,7 +67,7 @@ export default function Emoji({ ...@@ -66,7 +67,7 @@ export default function Emoji({
} }
export function generateEmoji(emoji: string) { export function generateEmoji(emoji: string) {
return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji( return `<img loading="lazy" class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(
emoji, emoji,
)}" />`; )}" />`;
} }
import { Attachment } from "revolt.js/dist/api/objects"; import { Attachment } from "revolt-api/types/Autumn";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
export interface IconBaseProps<T> { export interface IconBaseProps<T> {
...@@ -6,11 +6,13 @@ export interface IconBaseProps<T> { ...@@ -6,11 +6,13 @@ export interface IconBaseProps<T> {
attachment?: Attachment; attachment?: Attachment;
size: number; size: number;
hover?: boolean;
animate?: boolean; animate?: boolean;
} }
interface IconModifiers { interface IconModifiers {
square?: boolean; square?: boolean;
hover?: boolean;
} }
export default styled.svg<IconModifiers>` export default styled.svg<IconModifiers>`
...@@ -27,6 +29,14 @@ export default styled.svg<IconModifiers>` ...@@ -27,6 +29,14 @@ export default styled.svg<IconModifiers>`
border-radius: 50%; border-radius: 50%;
`} `}
} }
${(props) =>
props.hover &&
css`
&:hover .icon {
filter: brightness(0.8);
}
`}
`; `;
export const ImageIconBase = styled.img<IconModifiers>` export const ImageIconBase = styled.img<IconModifiers>`
...@@ -38,4 +48,12 @@ export const ImageIconBase = styled.img<IconModifiers>` ...@@ -38,4 +48,12 @@ export const ImageIconBase = styled.img<IconModifiers>`
css` css`
border-radius: 50%; border-radius: 50%;
`} `}
${(props) =>
props.hover &&
css`
&:hover img {
filter: brightness(0.8);
}
`}
`; `;
...@@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) { ...@@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) {
{Object.keys(Languages).map((x) => { {Object.keys(Languages).map((x) => {
const l = Languages[x as keyof typeof Languages]; const l = Languages[x as keyof typeof Languages];
return ( return (
<option value={x}> <option value={x} key={x}>
{l.emoji} {l.display} {l.emoji} {l.display}
</option> </option>
); );
......