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 (278)
Showing
with 2590 additions and 338 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
tabWidth: 4
\ No newline at end of file
module.exports = {
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
...@@ -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 ec907eb606a3e1d5046bef503caa0585f6bcbc22 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/fira-mono": "^4.4.5", {
"@fontsource/open-sans": "^4.4.5", "varsIgnorePattern": "^_"
"@hcaptcha/react-hcaptcha": "^0.3.6", }
"@preact/preset-vite": "^2.0.0", ],
"@rollup/plugin-replace": "^2.4.2", "no-unused-vars": [
"@styled-icons/boxicons-logos": "^10.34.0", "warn",
"@styled-icons/boxicons-regular": "^10.34.0", {
"@styled-icons/boxicons-solid": "^10.34.0", "varsIgnorePattern": "^_"
"@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", },
"@types/lodash.defaultsdeep": "^4.6.6", "dependencies": {
"@types/lodash.isequal": "^4.5.5", "vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
"@types/markdown-it": "^12.0.2", },
"@types/node": "^15.12.4", "devDependencies": {
"@types/preact-i18n": "^2.3.0", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@types/prismjs": "^1.16.5", "@fontsource/bree-serif": "^4.4.5",
"@types/react-helmet": "^6.1.1", "@fontsource/comic-neue": "^4.4.5",
"@types/react-router-dom": "^5.1.7", "@fontsource/fira-code": "^4.4.5",
"@types/react-scroll": "^1.8.2", "@fontsource/inter": "^4.4.5",
"@types/styled-components": "^5.1.10", "@fontsource/lato": "^4.4.5",
"@types/twemoji": "^12.1.1", "@fontsource/montserrat": "^4.4.5",
"@typescript-eslint/eslint-plugin": "^4.27.0", "@fontsource/noto-sans": "^4.4.5",
"@typescript-eslint/parser": "^4.27.0", "@fontsource/open-sans": "^4.4.5",
"classnames": "^2.3.1", "@fontsource/poppins": "^4.4.5",
"dayjs": "^1.10.5", "@fontsource/raleway": "^4.4.5",
"detect-browser": "^5.2.0", "@fontsource/roboto": "^4.4.5",
"eslint": "^7.28.0", "@fontsource/roboto-mono": "^4.4.5",
"eslint-config-preact": "^1.1.4", "@fontsource/source-code-pro": "^4.4.5",
"eventemitter3": "^4.0.7", "@fontsource/space-mono": "^4.4.5",
"highlight.js": "^11.0.1", "@fontsource/ubuntu": "^4.4.5",
"idb": "^6.1.2", "@fontsource/ubuntu-mono": "^4.4.5",
"localforage": "^1.9.0", "@hcaptcha/react-hcaptcha": "^0.3.6",
"lodash.defaultsdeep": "^4.6.1", "@preact/preset-vite": "^2.0.0",
"lodash.isequal": "^4.5.0", "@rollup/plugin-replace": "^2.4.2",
"markdown-it": "^12.0.6", "@styled-icons/boxicons-logos": "^10.34.0",
"markdown-it-emoji": "^2.0.0", "@styled-icons/boxicons-regular": "^10.34.0",
"markdown-it-sub": "^1.0.0", "@styled-icons/boxicons-solid": "^10.37.0",
"markdown-it-sup": "^1.0.0", "@styled-icons/simple-icons": "^10.33.0",
"mediasoup-client": "^3.6.33", "@tippyjs/react": "^4.2.5",
"preact-context-menu": "^0.1.5", "@traptitech/markdown-it-katex": "^3.4.3",
"preact-i18n": "^2.4.0-preactx", "@traptitech/markdown-it-spoiler": "^1.1.6",
"prettier": "^2.3.1", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
"prismjs": "^1.23.0", "@types/lodash.defaultsdeep": "^4.6.6",
"react-device-detect": "^1.17.0", "@types/lodash.isequal": "^4.5.5",
"react-helmet": "^6.1.0", "@types/markdown-it": "^12.0.2",
"react-hook-form": "6.3.0", "@types/node": "^15.12.4",
"react-overlapping-panels": "1.2.1", "@types/preact-i18n": "^2.3.0",
"react-redux": "^7.2.4", "@types/prismjs": "^1.16.5",
"react-router-dom": "^5.2.0", "@types/react-helmet": "^6.1.1",
"react-scroll": "^1.8.2", "@types/react-router-dom": "^5.1.7",
"redux": "^4.1.0", "@types/react-scroll": "^1.8.2",
"revolt.js": "4.3.3-alpha.6", "@types/styled-components": "^5.1.10",
"rimraf": "^3.0.2", "@types/twemoji": "^12.1.1",
"sass": "^1.35.1", "@typescript-eslint/eslint-plugin": "^4.27.0",
"shade-blend-color": "^1.0.0", "@typescript-eslint/parser": "^4.27.0",
"styled-components": "^5.3.0", "classnames": "^2.3.1",
"typescript": "^4.3.2", "dayjs": "^1.10.6",
"ulid": "^2.3.0", "detect-browser": "^5.2.0",
"use-resize-observer": "^7.0.0", "eslint": "^7.28.0",
"vite": "^2.3.7", "eslint-config-preact": "^1.1.4",
"vite-plugin-pwa": "^0.8.1", "eventemitter3": "^4.0.7",
"workbox-precaching": "^6.1.5" "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"
} }
[{
"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>
This diff is collapsed.
import message from './message.mp3'; import call_join from "./call_join.mp3";
import outbound from './outbound.mp3'; import call_leave from "./call_leave.mp3";
import call_join from './call_join.mp3'; import message from "./message.mp3";
import call_leave from './call_leave.mp3'; import outbound from "./outbound.mp3";
const SoundMap: { [key in Sounds]: string } = { const SoundMap: { [key in Sounds]: string } = {
message, message,
outbound, outbound,
call_join, call_join,
call_leave call_leave,
} };
export type Sounds = 'message' | 'outbound' | 'call_join' | 'call_leave'; export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
export const SOUNDS_ARRAY: Sounds[] = [ 'message', 'outbound', 'call_join', 'call_leave' ]; export const SOUNDS_ARRAY: Sounds[] = [
"message",
"outbound",
"call_join",
"call_leave",
];
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) {
console.error('Failed to play audio file', file, err); console.error("Failed to play audio file", file, 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>
);
});
This diff is collapsed.
import { useContext } from "preact/hooks";
import { Channels } from "revolt.js/dist/api/objects";
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular"; import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
import { ImageIconBase, IconBaseProps } from "./IconBase"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useContext } from "preact/hooks";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel> { import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png";
interface Props extends IconBaseProps<Channel> {
isServerChannel?: boolean; isServerChannel?: boolean;
} }
import fallback from './assets/group.png'; export default observer(
(
export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) { props: Props &
const client = useContext(AppContext); Omit<
JSX.HTMLAttributes<HTMLImageElement>,
const { size, target, attachment, isServerChannel: server, animate, children, as, ...imgProps } = props; keyof Props | "children" | "as"
const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate); >,
const isServerChannel = server || (target && (target.channel_type === 'TextChannel' || target.channel_type === 'VoiceChannel')); ) => {
const client = useContext(AppContext);
if (typeof iconURL === 'undefined') {
if (isServerChannel) { const {
if (target?.channel_type === 'VoiceChannel') { size,
return ( target,
<VolumeFull size={size} /> attachment,
) isServerChannel: server,
} else { animate,
return ( ...imgProps
<Hash size={size} /> } = 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} />;
}
return <Hash size={size} />;
} }
} }
}
return (
return ( // ! TODO: replace fallback with <picture /> + <source />
// ! fixme: 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 { ChevronDown } from "@styled-icons/boxicons-regular";
import { State, store } from "../../redux";
import { Action } from "../../redux/reducers";
import Details from "../ui/Details";
import { Children } from "../../types/Preact";
interface Props {
id: string;
defaultValue: boolean;
sticky?: boolean;
large?: boolean;
summary: Children;
children: Children;
}
export default function CollapsibleSection({
id,
defaultValue,
summary,
children,
...detailsProps
}: Props) {
const state: State = store.getState();
function setState(state: boolean) {
if (state === defaultValue) {
store.dispatch({
type: "SECTION_TOGGLE_UNSET",
id,
} as Action);
} else {
store.dispatch({
type: "SECTION_TOGGLE_SET",
id,
state,
} as Action);
}
}
return (
<Details
open={state.sectionToggle[id] ?? defaultValue}
onToggle={(e) => setState(e.currentTarget.open)}
{...detailsProps}>
<summary>
<div class="padding">
<ChevronDown size={20} />
{summary}
</div>
</summary>
{children}
</Details>
);
}
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) {
...@@ -13,7 +13,7 @@ function codePoints(rune: string) { ...@@ -13,7 +13,7 @@ function codePoints(rune: string) {
const pairs = []; const pairs = [];
let low = 0; let low = 0;
let i = 0; let i = 0;
while (i < rune.length) { while (i < rune.length) {
const charCode = rune.charCodeAt(i++); const charCode = rune.charCodeAt(i++);
if (low) { if (low) {
...@@ -25,7 +25,7 @@ function codePoints(rune: string) { ...@@ -25,7 +25,7 @@ function codePoints(rune: string) {
pairs.push(charCode); pairs.push(charCode);
} }
} }
return pairs; return pairs;
} }
...@@ -33,30 +33,41 @@ function codePoints(rune: string) { ...@@ -33,30 +33,41 @@ function codePoints(rune: string) {
// scripts/build.js#344 // scripts/build.js#344
// grabTheRightIcon(rawText); // grabTheRightIcon(rawText);
const UFE0Fg = /\uFE0F/g; const UFE0Fg = /\uFE0F/g;
const U200D = String.fromCharCode(0x200D); const U200D = String.fromCharCode(0x200d);
function toCodePoint(rune: string) { function toCodePoint(rune: string) {
return codePoints(rune.indexOf(U200D) < 0 ? rune.replace(UFE0Fg, '') : rune) return codePoints(rune.indexOf(U200D) < 0 ? rune.replace(UFE0Fg, "") : rune)
.map((val) => val.toString(16)) .map((val) => val.toString(16))
.join("-") .join("-");
} }
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}`;
} }
export default function Emoji({ emoji, size }: { emoji: string, size?: number }) { export default function Emoji({
emoji,
size,
}: {
emoji: string;
size?: number;
}) {
return ( return (
<img <img
alt={emoji} alt={emoji}
loading="lazy"
className="emoji" className="emoji"
draggable={false} draggable={false}
src={parseEmoji(emoji)} src={parseEmoji(emoji)}
style={size ? { width: `${size}px`, height: `${size}px` } : undefined} style={
size ? { width: `${size}px`, height: `${size}px` } : undefined
}
/> />
) );
} }
export function generateEmoji(emoji: string) { export function generateEmoji(emoji: string) {
return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(emoji)}" />`; return `<img loading="lazy" class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(
emoji,
)}" />`;
} }