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 (150)
Showing
with 533 additions and 355 deletions
......@@ -3,3 +3,4 @@ node_modules
dist
dist-ssr
*.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 = {
"tabWidth": 4,
"trailingComma": "all",
"jsxBracketSameLine": true,
"importOrder": ["preact|classnames|.scss$", "/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"],
"importOrderSeparation": true,
}
\ No newline at end of file
tabWidth: 4,
trailingComma: "all",
jsxBracketSameLine: true,
importOrder: [
"preact|classnames|.scss$",
"/(lib)",
"/(redux|mobx)",
"/(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
Subproject commit 846a0e8a3a36e606d4d4249e495dc0daeee9c65d
Subproject commit 09955e9d30c19c1a180fd3aacdb85961641da2bc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Revolt">
<head>
<meta charset="UTF-8" />
<title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Revolt" />
<!--<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<!--App Icons-->
<link rel="apple-touch-icon" href="public/assets/icons/apple-touch.png">
<link rel="icon" type="image/png" href="/src/assets/logo_round.png" />
<!--App Icons-->
<link
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-->
<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 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" />
<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" />
<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" />
<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 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>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<style>
html {
background-color: #191919;
}
</style>
</html>
\ No newline at end of file
<!--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
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"
/>
<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"
/>
<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"
/>
<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
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",
"scripts": {
"dev": "vite",
"build": "rimraf build && vite build",
"preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"preact",
"plugin:@typescript-eslint/recommended"
],
"ignorePatterns": [
"build/"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off"
}
},
"dependencies": {
"preact": "^10.5.13"
},
"devDependencies": {
"@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bree-serif": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5",
"@fontsource/lato": "^4.4.5",
"@fontsource/montserrat": "^4.4.5",
"@fontsource/noto-sans": "^4.4.5",
"@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",
"@fontsource/source-code-pro": "^4.4.5",
"@fontsource/space-mono": "^4.4.5",
"@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@preact/preset-vite": "^2.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.34.0",
"@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2",
"@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0",
"classnames": "^2.3.1",
"dayjs": "^1.10.6",
"detect-browser": "^5.2.0",
"eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7",
"highlight.js": "^11.0.1",
"idb": "^6.1.2",
"localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
"markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"preact-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
"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.js": "4.3.3-alpha.11",
"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": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5"
}
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "rimraf build && vite build",
"preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"preact",
"plugin:@typescript-eslint/recommended"
],
"ignorePatterns": [
"build/"
],
"rules": {
"radix": "off",
"no-spaced-func": "off",
"react/no-danger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
],
"no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
]
}
},
"dependencies": {
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
},
"devDependencies": {
"@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bree-serif": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5",
"@fontsource/lato": "^4.4.5",
"@fontsource/montserrat": "^4.4.5",
"@fontsource/noto-sans": "^4.4.5",
"@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",
"@fontsource/source-code-pro": "^4.4.5",
"@fontsource/space-mono": "^4.4.5",
"@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@preact/preset-vite": "^2.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.37.0",
"@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2",
"@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0",
"classnames": "^2.3.1",
"dayjs": "^1.10.6",
"detect-browser": "^5.2.0",
"eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7",
"highlight.js": "^11.0.1",
"localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
"markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.14",
"preact-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
"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"
}
export const emojiDictionary = {
"100": "💯",
"1234": "🔢",
100: "💯",
1234: "🔢",
grinning: "😀",
smiley: "😃",
smile: "😄",
......
......@@ -19,8 +19,8 @@ export const SOUNDS_ARRAY: Sounds[] = [
];
export function playSound(sound: Sounds) {
let file = SoundMap[sound];
let el = new Audio(file);
const file = SoundMap[sound];
const el = new Audio(file);
try {
el.play();
} catch (err) {
......
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { useState } from "preact/hooks";
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 Checkbox from "../ui/Checkbox";
import Button from "../ui/Button";
import Checkbox from "../ui/Checkbox";
import { Children } from "../../types/Preact";
import { Channel } from "revolt.js";
import { Text } from "preact-i18n";
const Base = styled.div`
display: flex;
......@@ -38,53 +43,67 @@ type Props = {
gated: boolean;
children: Children;
} & {
type: 'channel';
type: "channel";
channel: Channel;
}
};
export default function AgeGate(props: Props) {
export default observer((props: Props) => {
const history = useHistory();
const [consent, setConsent] = useState(getState().sectionToggle['nsfw'] ?? false);
const [consent, setConsent] = useState(
getState().sectionToggle["nsfw"] ?? false,
);
const [ageGate, setAgeGate] = useState(false);
if (ageGate || !props.gated) {
return <>{ props.children }</>;
} else {
if (!(props.channel.channel_type === 'Group' || props.channel.channel_type === 'TextChannel')) return <>{ props.children }</>;
return <>{props.children}</>;
}
if (
!(
props.channel.channel_type === "Group" ||
props.channel.channel_type === "TextChannel"
)
)
return <>{props.children}</>;
return (
<Base>
<img
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>
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) => {
<Checkbox
checked={consent}
onChange={(v) => {
setConsent(v);
if (v) {
dispatch({ type: 'SECTION_TOGGLE_SET', id: 'nsfw', state: true });
dispatch({
type: "SECTION_TOGGLE_SET",
id: "nsfw",
state: true,
});
} else {
dispatch({ type: 'SECTION_TOGGLE_UNSET', id: 'nsfw' });
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>
);
}
}
<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 { Channels } from "revolt.js/dist/api/objects";
import { SYSTEM_USER_ID } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
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 ChannelIcon from "./ChannelIcon";
......@@ -24,7 +25,7 @@ export type AutoCompleteState =
}
| {
type: "channel";
matches: Channels.TextChannel[];
matches: Channel[];
}
));
......@@ -52,16 +53,16 @@ export function useAutoComplete(
): AutoCompleteProps {
const [state, setState] = useState<AutoCompleteState>({ type: "none" });
const [focused, setFocused] = useState(false);
const client = useContext(AppContext);
const client = useClient();
function findSearchString(
el: HTMLTextAreaElement,
): ["emoji" | "user" | "channel", string, number] | undefined {
if (el.selectionStart === el.selectionEnd) {
let cursor = el.selectionStart;
let content = el.value.slice(0, cursor);
const cursor = el.selectionStart;
const content = el.value.slice(0, cursor);
let valid = /\w/;
const valid = /\w/;
let j = content.length - 1;
if (content[j] === "@") {
......@@ -75,10 +76,10 @@ export function useAutoComplete(
}
if (j === -1) return;
let current = content[j];
const current = content[j];
if (current === ":" || current === "@" || current === "#") {
let search = content.slice(j + 1, content.length);
const search = content.slice(j + 1, content.length);
if (search.length > 0) {
return [
current === "#"
......@@ -97,19 +98,19 @@ export function useAutoComplete(
function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
const el = ev.currentTarget;
let result = findSearchString(el);
const result = findSearchString(el);
if (result) {
let [type, search] = result;
const [type, search] = result;
const regex = new RegExp(search, "i");
if (type === "emoji") {
// ! FIXME: we should convert it to a Binary Search Tree and use that
let matches = Object.keys(emojiDictionary)
// ! TODO: we should convert it to a Binary Search Tree and use that
const matches = Object.keys(emojiDictionary)
.filter((emoji: string) => emoji.match(regex))
.splice(0, 5);
if (matches.length > 0) {
let currentPosition =
const currentPosition =
state.type !== "none" ? state.selected : 0;
setState({
......@@ -127,32 +128,30 @@ export function useAutoComplete(
let users: User[] = [];
switch (searchClues.users.type) {
case "all":
users = client.users.toArray();
users = [...client.users.values()];
break;
case "channel": {
let channel = client.channels.get(searchClues.users.id);
const channel = client.channels.get(
searchClues.users.id,
);
switch (channel?.channel_type) {
case "Group":
case "DirectMessage":
users = client.users
.mapKeys(channel.recipients)
.filter(
(x) => typeof x !== "undefined",
) as User[];
users = channel.recipients!.filter(
(x) => typeof x !== "undefined",
) as User[];
break;
case "TextChannel":
const server = channel.server;
users = client.servers.members
.toArray()
.filter(
(x) => x._id.substr(0, 26) === server,
)
.map((x) =>
client.users.get(x._id.substr(26)),
)
.filter(
(x) => typeof x !== "undefined",
) as User[];
{
const server = channel.server_id;
users = [...client.members.keys()]
.map((x) => JSON.parse(x))
.filter((x) => x.server === server)
.map((x) => client.users.get(x.user))
.filter(
(x) => typeof x !== "undefined",
) as User[];
}
break;
default:
return;
......@@ -162,7 +161,7 @@ export function useAutoComplete(
users = users.filter((x) => x._id !== SYSTEM_USER_ID);
let matches = (
const matches = (
search.length > 0
? users.filter((user) =>
user.username.toLowerCase().match(regex),
......@@ -173,7 +172,7 @@ export function useAutoComplete(
.filter((x) => typeof x !== "undefined");
if (matches.length > 0) {
let currentPosition =
const currentPosition =
state.type !== "none" ? state.selected : 0;
setState({
......@@ -188,17 +187,16 @@ export function useAutoComplete(
}
if (type === "channel" && searchClues?.channels) {
let channels = client.servers
const channels = client.servers
.get(searchClues.channels.server)
?.channels.map((x) => client.channels.get(x))
.filter(
?.channels.filter(
(x) => typeof x !== "undefined",
) as Channels.TextChannel[];
) as Channel[];
let matches = (
const matches = (
search.length > 0
? channels.filter((channel) =>
channel.name.toLowerCase().match(regex),
channel.name!.toLowerCase().match(regex),
)
: channels
)
......@@ -206,7 +204,7 @@ export function useAutoComplete(
.filter((x) => typeof x !== "undefined");
if (matches.length > 0) {
let currentPosition =
const currentPosition =
state.type !== "none" ? state.selected : 0;
setState({
......@@ -228,11 +226,11 @@ export function useAutoComplete(
function selectCurrent(el: HTMLTextAreaElement) {
if (state.type !== "none") {
let result = findSearchString(el);
const result = findSearchString(el);
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") {
content.splice(
index,
......@@ -307,7 +305,7 @@ export function useAutoComplete(
function onKeyUp(e: KeyboardEvent) {
if (e.currentTarget !== null) {
// @ts-expect-error
// @ts-expect-error Type mis-match.
onChange(e);
}
}
......@@ -353,13 +351,13 @@ const Base = styled.div<{ detached?: boolean }>`
display: flex;
font-size: 1em;
cursor: pointer;
border-radius: 6px;
align-items: center;
flex-direction: row;
font-family: inherit;
background: transparent;
color: var(--foreground);
width: calc(100% - 12px);
font-family: inherit;
border-radius: var(--border-radius);
span {
display: grid;
......@@ -377,7 +375,7 @@ const Base = styled.div<{ detached?: boolean }>`
bottom: 8px;
> div {
border-radius: 4px;
border-radius: var(--border-radius);
}
`}
`;
......@@ -394,6 +392,7 @@ export default function AutoComplete({
{state.type === "emoji" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
......@@ -425,6 +424,7 @@ export default function AutoComplete({
{state.type === "user" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
......@@ -449,6 +449,7 @@ export default function AutoComplete({
{state.type === "channel" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
......
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";
......@@ -8,58 +9,59 @@ import { AppContext } from "../../context/revoltjs/RevoltClient";
import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png";
interface Props
extends IconBaseProps<
Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel
> {
interface Props extends IconBaseProps<Channel> {
isServerChannel?: boolean;
}
export default function ChannelIcon(
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
) {
const client = useContext(AppContext);
export default observer(
(
props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext);
const {
size,
target,
attachment,
isServerChannel: server,
animate,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
animate,
);
const isServerChannel =
server ||
(target &&
(target.channel_type === "TextChannel" ||
target.channel_type === "VoiceChannel"));
const {
size,
target,
attachment,
isServerChannel: server,
animate,
...imgProps
} = props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
animate,
);
const isServerChannel =
server ||
(target &&
(target.channel_type === "TextChannel" ||
target.channel_type === "VoiceChannel"));
if (typeof iconURL === "undefined") {
if (isServerChannel) {
if (target?.channel_type === "VoiceChannel") {
return <VolumeFull size={size} />;
} else {
if (typeof iconURL === "undefined") {
if (isServerChannel) {
if (target?.channel_type === "VoiceChannel") {
return <VolumeFull size={size} />;
}
return <Hash size={size} />;
}
}
}
return (
// ! fixme: replace fallback with <picture /> + <source />
<ImageIconBase
{...imgProps}
width={size}
height={size}
aria-hidden="true"
square={isServerChannel}
src={iconURL ?? fallback}
/>
);
}
return (
// ! TODO: replace fallback with <picture /> + <source />
<ImageIconBase
{...imgProps}
width={size}
height={size}
loading="lazy"
aria-hidden="true"
square={isServerChannel}
src={iconURL ?? fallback}
/>
);
},
);
import { EmojiPacks } from "../../redux/reducers/settings";
var EMOJI_PACK = "mutant";
let EMOJI_PACK = "mutant";
const REVISION = 3;
export function setEmojiPack(pack: EmojiPacks) {
......@@ -41,7 +41,7 @@ function toCodePoint(rune: 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}`;
}
......@@ -55,6 +55,7 @@ export default function Emoji({
return (
<img
alt={emoji}
loading="lazy"
className="emoji"
draggable={false}
src={parseEmoji(emoji)}
......@@ -66,7 +67,7 @@ export default function Emoji({
}
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,
)}" />`;
}
import { Attachment } from "revolt.js/dist/api/objects";
import { Attachment } from "revolt-api/types/Autumn";
import styled, { css } from "styled-components";
export interface IconBaseProps<T> {
......@@ -6,11 +6,13 @@ export interface IconBaseProps<T> {
attachment?: Attachment;
size: number;
hover?: boolean;
animate?: boolean;
}
interface IconModifiers {
square?: boolean;
hover?: boolean;
}
export default styled.svg<IconModifiers>`
......@@ -27,6 +29,14 @@ export default styled.svg<IconModifiers>`
border-radius: 50%;
`}
}
${(props) =>
props.hover &&
css`
&:hover .icon {
filter: brightness(0.8);
}
`}
`;
export const ImageIconBase = styled.img<IconModifiers>`
......@@ -38,4 +48,12 @@ export const ImageIconBase = styled.img<IconModifiers>`
css`
border-radius: 50%;
`}
${(props) =>
props.hover &&
css`
&:hover img {
filter: brightness(0.8);
}
`}
`;
......@@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) {
{Object.keys(Languages).map((x) => {
const l = Languages[x as keyof typeof Languages];
return (
<option value={x}>
<option value={x} key={x}>
{l.emoji} {l.display}
</option>
);
......
import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
import { Server } from "revolt.js/dist/api/objects";
import { ServerPermission } from "revolt.js/dist/api/permissions";
import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components";
import { HookContext, useServerPermission } from "../../context/revoltjs/hooks";
import Header from "../ui/Header";
import IconButton from "../ui/IconButton";
interface Props {
server: Server;
ctx: HookContext;
}
const ServerName = styled.div`
flex-grow: 1;
`;
export default function ServerHeader({ server, ctx }: Props) {
const permissions = useServerPermission(server._id, ctx);
const bannerURL = ctx.client.servers.getBannerURL(
server._id,
{ width: 480 },
true,
);
export default observer(({ server }: Props) => {
const bannerURL = server.generateBannerURL({ width: 480 });
return (
<Header
......@@ -35,7 +28,7 @@ export default function ServerHeader({ server, ctx }: Props) {
background: bannerURL ? `url('${bannerURL}')` : undefined,
}}>
<ServerName>{server.name}</ServerName>
{(permissions & ServerPermission.ManageServer) > 0 && (
{(server.permission & ServerPermission.ManageServer) > 0 && (
<div className="actions">
<Link to={`/server/${server._id}/settings`}>
<IconButton>
......@@ -46,4 +39,4 @@ export default function ServerHeader({ server, ctx }: Props) {
)}
</Header>
);
}
});
import { Server } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components";
import { useContext } from "preact/hooks";
......@@ -21,48 +22,47 @@ const ServerText = styled.div`
background: var(--primary-background);
`;
const fallback = "/assets/group.png";
export default function ServerIcon(
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
) {
const client = useContext(AppContext);
// const fallback = "/assets/group.png";
export default observer(
(
props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext);
const {
target,
attachment,
size,
animate,
server_name,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
animate,
);
const { target, attachment, size, animate, server_name, ...imgProps } =
props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
animate,
);
if (typeof iconURL === "undefined") {
const name = target?.name ?? server_name ?? "";
if (typeof iconURL === "undefined") {
const name = target?.name ?? server_name ?? "";
return (
<ServerText style={{ width: size, height: size }}>
{name
.split(" ")
.map((x) => x[0])
.filter((x) => typeof x !== "undefined")}
</ServerText>
);
}
return (
<ServerText style={{ width: size, height: size }}>
{name
.split(" ")
.map((x) => x[0])
.filter((x) => typeof x !== "undefined")}
</ServerText>
<ImageIconBase
{...imgProps}
width={size}
height={size}
src={iconURL}
loading="lazy"
aria-hidden="true"
/>
);
}
return (
<ImageIconBase
{...imgProps}
width={size}
height={size}
aria-hidden="true"
src={iconURL}
/>
);
}
},
);
......@@ -16,8 +16,8 @@ export default function Tooltip(props: Props) {
return (
<Tippy content={content} {...tippyProps}>
{/*
// @ts-expect-error */}
<div>{children}</div>
// @ts-expect-error Type mis-match. */}
<div style={`display: flex;`}>{children}</div>
</Tippy>
);
}
......@@ -35,7 +35,7 @@ const PermissionTooltipBase = styled.div`
}
code {
font-family: var(--monoscape-font);
font-family: var(--monospace-font);
}
`;
......
import { Download } from "@styled-icons/boxicons-regular";
/* eslint-disable react-hooks/rules-of-hooks */
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
import { useContext, useEffect, useState } from "preact/hooks";
......@@ -9,11 +10,16 @@ import { ThemeContext } from "../../context/Theme";
import IconButton from "../ui/IconButton";
import { updateSW } from "../../main";
import Tooltip from "./Tooltip";
var pendingUpdate = false;
let pendingUpdate = false;
internalSubscribe("PWA", "update", () => (pendingUpdate = true));
export default function UpdateIndicator() {
interface Props {
style: "titlebar" | "channel";
}
export default function UpdateIndicator({ style }: Props) {
const [pending, setPending] = useState(pendingUpdate);
useEffect(() => {
......@@ -23,6 +29,22 @@ export default function UpdateIndicator() {
if (!pending) return null;
const theme = useContext(ThemeContext);
if (style === "titlebar") {
return (
<div class="actions">
<Tooltip
content="A new update is available!"
placement="bottom">
<div onClick={() => updateSW(true)}>
<CloudDownload size={22} color={theme.success} />
</div>
</Tooltip>
</div>
);
}
if (window.isNative) return null;
return (
<IconButton onClick={() => updateSW(true)}>
<Download size={22} color={theme.success} />
......