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 (92)
Showing
with 554 additions and 400 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
{ {
"recommendations": ["esbenp.prettier-vscode"] "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
} }
Subproject commit f3c991242044c0125111e533a94350633a681d62 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.6", "@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": "npm:@insertish/mediasoup-client@3.6.36-esnext", "@typescript-eslint/parser": "^4.27.0",
"mobx": "^6.3.2", "classnames": "^2.3.1",
"mobx-react-lite": "^3.2.0", "dayjs": "^1.10.6",
"preact-context-menu": "^0.1.5", "detect-browser": "^5.2.0",
"preact-i18n": "^2.4.0-preactx", "eslint": "^7.28.0",
"prettier": "^2.3.1", "eslint-config-preact": "^1.1.4",
"prismjs": "^1.23.0", "eventemitter3": "^4.0.7",
"react-device-detect": "^1.17.0", "highlight.js": "^11.0.1",
"react-helmet": "^6.1.0", "localforage": "^1.9.0",
"react-hook-form": "6.3.0", "lodash.defaultsdeep": "^4.6.1",
"react-overlapping-panels": "1.2.2", "lodash.isequal": "^4.5.0",
"react-redux": "^7.2.4", "markdown-it": "^12.0.6",
"react-router-dom": "^5.2.0", "markdown-it-emoji": "^2.0.0",
"react-scroll": "^1.8.2", "markdown-it-sub": "^1.0.0",
"redux": "^4.1.0", "markdown-it-sup": "^1.0.0",
"revolt.js": "^4.3.3-alpha.19", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"rimraf": "^3.0.2", "mobx": "^6.3.2",
"sass": "^1.35.1", "mobx-react-lite": "^3.2.0",
"shade-blend-color": "^1.0.0", "preact": "^10.5.14",
"styled-components": "^5.3.0", "preact-context-menu": "^0.1.5",
"typescript": "^4.3.2", "preact-i18n": "^2.4.0-preactx",
"ulid": "^2.3.0", "prettier": "^2.3.1",
"use-resize-observer": "^7.0.0", "prismjs": "^1.23.0",
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b", "react-device-detect": "^1.17.0",
"vite-plugin-pwa": "^0.8.1", "react-helmet": "^6.1.0",
"workbox-precaching": "^6.1.5" "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"
} }
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -46,7 +47,7 @@ type Props = { ...@@ -46,7 +47,7 @@ type Props = {
channel: Channel; channel: Channel;
}; };
export default function AgeGate(props: Props) { export default observer((props: Props) => {
const history = useHistory(); const history = useHistory();
const [consent, setConsent] = useState( const [consent, setConsent] = useState(
getState().sectionToggle["nsfw"] ?? false, getState().sectionToggle["nsfw"] ?? false,
...@@ -105,4 +106,4 @@ export default function AgeGate(props: Props) { ...@@ -105,4 +106,4 @@ export default function AgeGate(props: Props) {
</div> </div>
</Base> </Base>
); );
} });
import { useStore } from "react-redux";
import { SYSTEM_USER_ID } 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, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";
import { User } from "../../mobx";
import { useData } from "../../mobx/State";
import { useClient } from "../../context/revoltjs/RevoltClient"; import { useClient } from "../../context/revoltjs/RevoltClient";
import { emojiDictionary } from "../../assets/emojis"; import { emojiDictionary } from "../../assets/emojis";
...@@ -28,7 +25,7 @@ export type AutoCompleteState = ...@@ -28,7 +25,7 @@ export type AutoCompleteState =
} }
| { | {
type: "channel"; type: "channel";
matches: Channels.TextChannel[]; matches: Channel[];
} }
)); ));
...@@ -57,7 +54,6 @@ export function useAutoComplete( ...@@ -57,7 +54,6 @@ export function useAutoComplete(
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 = useClient(); const client = useClient();
const store = useData();
function findSearchString( function findSearchString(
el: HTMLTextAreaElement, el: HTMLTextAreaElement,
...@@ -108,7 +104,7 @@ export function useAutoComplete( ...@@ -108,7 +104,7 @@ export function useAutoComplete(
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
const 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);
...@@ -132,7 +128,7 @@ export function useAutoComplete( ...@@ -132,7 +128,7 @@ export function useAutoComplete(
let users: User[] = []; let users: User[] = [];
switch (searchClues.users.type) { switch (searchClues.users.type) {
case "all": case "all":
users = [...store.users.values()]; users = [...client.users.values()];
break; break;
case "channel": { case "channel": {
const channel = client.channels.get( const channel = client.channels.get(
...@@ -141,25 +137,21 @@ export function useAutoComplete( ...@@ -141,25 +137,21 @@ export function useAutoComplete(
switch (channel?.channel_type) { switch (channel?.channel_type) {
case "Group": case "Group":
case "DirectMessage": case "DirectMessage":
users = channel.recipients users = channel.recipients!.filter(
.map((x) => store.users.get(x)) (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.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(
store.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;
...@@ -197,15 +189,14 @@ export function useAutoComplete( ...@@ -197,15 +189,14 @@ export function useAutoComplete(
if (type === "channel" && searchClues?.channels) { if (type === "channel" && searchClues?.channels) {
const 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[];
const 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
) )
...@@ -314,7 +305,7 @@ export function useAutoComplete( ...@@ -314,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);
} }
} }
...@@ -401,6 +392,7 @@ export default function AutoComplete({ ...@@ -401,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) &&
...@@ -432,6 +424,7 @@ export default function AutoComplete({ ...@@ -432,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) &&
...@@ -456,6 +449,7 @@ export default function AutoComplete({ ...@@ -456,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} />;
}
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}
loading="lazy" loading="lazy"
aria-hidden="true" aria-hidden="true"
square={isServerChannel} square={isServerChannel}
src={iconURL ?? fallback} src={iconURL ?? fallback}
/> />
); );
} },
);
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>
); );
......
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Server } from "revolt.js/dist/api/objects";
import { ServerPermission } from "revolt.js/dist/api/permissions"; import { ServerPermission } from "revolt.js/dist/api/permissions";
import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components"; import styled from "styled-components";
import { HookContext, useServerPermission } from "../../context/revoltjs/hooks";
import Header from "../ui/Header"; import Header from "../ui/Header";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
interface Props { interface Props {
server: Server; server: Server;
ctx: HookContext;
} }
const ServerName = styled.div` const ServerName = styled.div`
flex-grow: 1; flex-grow: 1;
`; `;
export default function ServerHeader({ server, ctx }: Props) { export default observer(({ server }: Props) => {
const permissions = useServerPermission(server._id, ctx); const bannerURL = server.generateBannerURL({ width: 480 });
const bannerURL = ctx.client.servers.getBannerURL(
server._id,
{ width: 480 },
true,
);
return ( return (
<Header <Header
...@@ -35,7 +28,7 @@ export default function ServerHeader({ server, ctx }: Props) { ...@@ -35,7 +28,7 @@ export default function ServerHeader({ server, ctx }: Props) {
background: bannerURL ? `url('${bannerURL}')` : undefined, background: bannerURL ? `url('${bannerURL}')` : undefined,
}}> }}>
<ServerName>{server.name}</ServerName> <ServerName>{server.name}</ServerName>
{(permissions & ServerPermission.ManageServer) > 0 && ( {(server.permission & ServerPermission.ManageServer) > 0 && (
<div className="actions"> <div className="actions">
<Link to={`/server/${server._id}/settings`}> <Link to={`/server/${server._id}/settings`}>
<IconButton> <IconButton>
...@@ -46,4 +39,4 @@ export default function ServerHeader({ server, ctx }: Props) { ...@@ -46,4 +39,4 @@ export default function ServerHeader({ server, ctx }: Props) {
)} )}
</Header> </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 styled from "styled-components";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
...@@ -21,49 +22,47 @@ const ServerText = styled.div` ...@@ -21,49 +22,47 @@ const ServerText = styled.div`
background: var(--primary-background); background: var(--primary-background);
`; `;
const fallback = "/assets/group.png"; // const fallback = "/assets/group.png";
export default function ServerIcon( 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 { target, attachment, size, animate, server_name, ...imgProps } =
target, props;
attachment, const iconURL = client.generateFileURL(
size, target?.icon ?? attachment,
animate, { max_side: 256 },
server_name, animate,
children, );
as,
...imgProps if (typeof iconURL === "undefined") {
} = props; const name = target?.name ?? server_name ?? "";
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
animate,
);
if (typeof iconURL === "undefined") { return (
const name = target?.name ?? server_name ?? ""; <ServerText style={{ width: size, height: size }}>
{name
.split(" ")
.map((x) => x[0])
.filter((x) => typeof x !== "undefined")}
</ServerText>
);
}
return ( return (
<ServerText style={{ width: size, height: size }}> <ImageIconBase
{name {...imgProps}
.split(" ") width={size}
.map((x) => x[0]) height={size}
.filter((x) => typeof x !== "undefined")} src={iconURL}
</ServerText> loading="lazy"
aria-hidden="true"
/>
); );
} },
);
return (
<ImageIconBase
{...imgProps}
width={size}
height={size}
src={iconURL}
loading="lazy"
aria-hidden="true"
/>
);
}
...@@ -16,8 +16,8 @@ export default function Tooltip(props: Props) { ...@@ -16,8 +16,8 @@ export default function Tooltip(props: Props) {
return ( return (
<Tippy content={content} {...tippyProps}> <Tippy content={content} {...tippyProps}>
{/* {/*
// @ts-expect-error */} // @ts-expect-error Type mis-match. */}
<div>{children}</div> <div style={`display: flex;`}>{children}</div>
</Tippy> </Tippy>
); );
} }
......
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"; import { useContext, useEffect, useState } from "preact/hooks";
...@@ -9,11 +10,16 @@ import { ThemeContext } from "../../context/Theme"; ...@@ -9,11 +10,16 @@ import { ThemeContext } from "../../context/Theme";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
import { updateSW } from "../../main"; import { updateSW } from "../../main";
import Tooltip from "./Tooltip";
let pendingUpdate = false; let pendingUpdate = false;
internalSubscribe("PWA", "update", () => (pendingUpdate = true)); 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); const [pending, setPending] = useState(pendingUpdate);
useEffect(() => { useEffect(() => {
...@@ -23,6 +29,22 @@ export default function UpdateIndicator() { ...@@ -23,6 +29,22 @@ export default function UpdateIndicator() {
if (!pending) return null; if (!pending) return null;
const theme = useContext(ThemeContext); 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 ( return (
<IconButton onClick={() => updateSW(true)}> <IconButton onClick={() => updateSW(true)}>
<Download size={22} color={theme.success} /> <Download size={22} color={theme.success} />
......
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
import { attachContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useContext, useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useData } from "../../../mobx/State";
import { QueuedMessage } from "../../../redux/reducers/queue"; import { QueuedMessage } from "../../../redux/reducers/queue";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import { useUser } from "../../../context/revoltjs/hooks";
import { MessageObject } from "../../../context/revoltjs/util";
import Overline from "../../ui/Overline"; import Overline from "../../ui/Overline";
...@@ -47,54 +45,60 @@ const Message = observer( ...@@ -47,54 +45,60 @@ const Message = observer(
head: preferHead, head: preferHead,
queued, queued,
}: Props) => { }: Props) => {
const store = useData(); const client = useClient();
const user = store.users.get(message.author); const user = message.author;
const client = useContext(AppContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const content = message.content as string; const content = message.content as string;
const head = const head =
preferHead || (message.replies && message.replies.length > 0); preferHead || (message.reply_ids && message.reply_ids.length > 0);
// ! FIXME: tell fatal to make this type generic // ! TODO: tell fatal to make this type generic
// bree: Fatal please... // bree: Fatal please...
const userContext = attachContext const userContext = attachContext
? (attachContextMenu("Menu", { ? (attachContextMenu("Menu", {
user: message.author, user: message.author_id,
contextualChannel: message.channel, contextualChannel: message.channel_id,
// eslint-disable-next-line
}) as any) }) as any)
: undefined; : undefined;
const openProfile = () => const openProfile = () =>
openScreen({ id: "profile", user_id: message.author }); openScreen({ id: "profile", user_id: message.author_id });
// ! FIXME: animate on hover // ! FIXME(?): animate on hover
const [animate, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
return ( return (
<div id={message._id}> <div id={message._id}>
{message.replies?.map((message_id, index) => ( {message.reply_ids?.map((message_id, index) => (
<MessageReply <MessageReply
key={message_id}
index={index} index={index}
id={message_id} id={message_id}
channel={message.channel} channel={message.channel!}
/> />
))} ))}
<MessageBase <MessageBase
highlight={highlight} highlight={highlight}
head={ head={
head && !(message.replies && message.replies.length > 0) (head &&
!(
message.reply_ids &&
message.reply_ids.length > 0
)) ??
false
} }
contrast={contrast} contrast={contrast}
sending={typeof queued !== "undefined"} sending={typeof queued !== "undefined"}
mention={message.mentions?.includes(client.user!._id)} mention={message.mention_ids?.includes(client.user!._id)}
failed={typeof queued?.error !== "undefined"} failed={typeof queued?.error !== "undefined"}
onContextMenu={ onContextMenu={
attachContext attachContext
? attachContextMenu("Menu", { ? attachContextMenu("Menu", {
message, message,
contextualChannel: message.channel, contextualChannel: message.channel_id,
queued, queued,
}) })
: undefined : undefined
......
import { observer } from "mobx-react-lite";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
...@@ -6,7 +8,6 @@ import { Text } from "preact-i18n"; ...@@ -6,7 +8,6 @@ import { Text } from "preact-i18n";
import { useDictionary } from "../../../lib/i18n"; import { useDictionary } from "../../../lib/i18n";
import { dayjs } from "../../../context/Locale"; import { dayjs } from "../../../context/Locale";
import { MessageObject } from "../../../context/revoltjs/util";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
...@@ -31,7 +32,11 @@ export default styled.div<BaseMessageProps>` ...@@ -31,7 +32,11 @@ export default styled.div<BaseMessageProps>`
overflow: none; overflow: none;
padding: 0.125rem; padding: 0.125rem;
flex-direction: row; flex-direction: row;
padding-right: 16px; padding-inline-end: 16px;
@media (pointer: coarse) {
user-select: none;
}
${(props) => ${(props) =>
props.contrast && props.contrast &&
...@@ -89,12 +94,20 @@ export default styled.div<BaseMessageProps>` ...@@ -89,12 +94,20 @@ export default styled.div<BaseMessageProps>`
gap: 8px; gap: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
} }
.author { .author {
overflow: hidden;
cursor: pointer; cursor: pointer;
font-weight: 600 !important; font-weight: 600 !important;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
white-space: normal;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
...@@ -178,6 +191,7 @@ export const MessageContent = styled.div` ...@@ -178,6 +191,7 @@ export const MessageContent = styled.div`
`; `;
export const DetailBase = styled.div` export const DetailBase = styled.div`
flex-shrink: 0;
gap: 4px; gap: 4px;
font-size: 10px; font-size: 10px;
display: inline-flex; display: inline-flex;
...@@ -192,57 +206,54 @@ export const DetailBase = styled.div` ...@@ -192,57 +206,54 @@ export const DetailBase = styled.div`
} }
`; `;
export function MessageDetail({ export const MessageDetail = observer(
message, ({ message, position }: { message: Message; position: "left" | "top" }) => {
position, const dict = useDictionary();
}: {
message: MessageObject; if (position === "left") {
position: "left" | "top"; if (message.edited) {
}) { return (
const dict = useDictionary(); <>
<time className="copyTime">
if (position === "left") { <i className="copyBracket">[</i>
if (message.edited) { {dayjs(decodeTime(message._id)).format(
dict.dayjs?.timeFormat,
)}
<i className="copyBracket">]</i>
</time>
<span className="edited">
<Tooltip
content={dayjs(message.edited).format("LLLL")}>
<Text id="app.main.channel.edited" />
</Tooltip>
</span>
</>
);
}
return ( return (
<> <>
<time className="copyTime"> <time>
<i className="copyBracket">[</i> <i className="copyBracket">[</i>
{dayjs(decodeTime(message._id)).format( {dayjs(decodeTime(message._id)).format(
dict.dayjs.timeFormat, dict.dayjs?.timeFormat,
)} )}
<i className="copyBracket">]</i> <i className="copyBracket">]</i>
</time> </time>
<span className="edited">
<Tooltip content={dayjs(message.edited).format("LLLL")}>
<Text id="app.main.channel.edited" />
</Tooltip>
</span>
</> </>
); );
} }
return ( return (
<> <DetailBase>
<time> <time>{dayjs(decodeTime(message._id)).calendar()}</time>
<i className="copyBracket">[</i> {message.edited && (
{dayjs(decodeTime(message._id)).format( <Tooltip content={dayjs(message.edited).format("LLLL")}>
dict.dayjs.timeFormat, <span className="edited">
)} <Text id="app.main.channel.edited" />
<i className="copyBracket">]</i> </span>
</time> </Tooltip>
</> )}
</DetailBase>
); );
} },
);
return (
<DetailBase>
<time>{dayjs(decodeTime(message._id)).calendar()}</time>
{message.edited && (
<Tooltip content={dayjs(message.edited).format("LLLL")}>
<span className="edited">
<Text id="app.main.channel.edited" />
</span>
</Tooltip>
)}
</DetailBase>
);
}
import { Send, HappyAlt, ShieldX } from "@styled-icons/boxicons-solid"; import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import { Styleshare } from "@styled-icons/simple-icons";
import Axios, { CancelTokenSource } from "axios"; import Axios, { CancelTokenSource } from "axios";
import { Channel } from "revolt.js"; import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js/dist/api/permissions"; import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { ulid } from "ulid"; import { ulid } from "ulid";
...@@ -31,7 +31,6 @@ import { ...@@ -31,7 +31,6 @@ import {
uploadFile, uploadFile,
} from "../../../context/revoltjs/FileUploads"; } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useChannelPermission } from "../../../context/revoltjs/hooks";
import { takeError } from "../../../context/revoltjs/util"; import { takeError } from "../../../context/revoltjs/util";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
...@@ -110,7 +109,7 @@ const Action = styled.div` ...@@ -110,7 +109,7 @@ const Action = styled.div`
// ! FIXME: add to app config and load from app config // ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 4; export const CAN_UPLOAD_AT_ONCE = 4;
export default function MessageBox({ channel }: Props) { export default observer(({ channel }: Props) => {
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? ""); const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
const [uploadState, setUploadState] = useState<UploadState>({ const [uploadState, setUploadState] = useState<UploadState>({
...@@ -123,8 +122,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -123,8 +122,7 @@ export default function MessageBox({ channel }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const translate = useTranslation(); const translate = useTranslation();
const permissions = useChannelPermission(channel._id); if (!(channel.permission & ChannelPermission.SendMessage)) {
if (!(permissions & ChannelPermission.SendMessage)) {
return ( return (
<Base> <Base>
<Blocked> <Blocked>
...@@ -143,22 +141,25 @@ export default function MessageBox({ channel }: Props) { ...@@ -143,22 +141,25 @@ export default function MessageBox({ channel }: Props) {
); );
} }
function setMessage(content?: string) { const setMessage = useCallback(
setDraft(content ?? ""); (content?: string) => {
setDraft(content ?? "");
if (content) { if (content) {
dispatch({ dispatch({
type: "SET_DRAFT", type: "SET_DRAFT",
channel: channel._id, channel: channel._id,
content, content,
}); });
} else { } else {
dispatch({ dispatch({
type: "CLEAR_DRAFT", type: "CLEAR_DRAFT",
channel: channel._id, channel: channel._id,
}); });
} }
} },
[channel._id],
);
useEffect(() => { useEffect(() => {
function append(content: string, action: "quote" | "mention") { function append(content: string, action: "quote" | "mention") {
...@@ -177,8 +178,12 @@ export default function MessageBox({ channel }: Props) { ...@@ -177,8 +178,12 @@ export default function MessageBox({ channel }: Props) {
} }
} }
return internalSubscribe("MessageBox", "append", append); return internalSubscribe(
}, [draft]); "MessageBox",
"append",
append as (...args: unknown[]) => void,
);
}, [draft, setMessage]);
async function send() { async function send() {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
...@@ -216,7 +221,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -216,7 +221,7 @@ export default function MessageBox({ channel }: Props) {
); );
try { try {
await client.channels.sendMessage(channel._id, { await channel.sendMessage({
content, content,
nonce, nonce,
replies, replies,
...@@ -290,7 +295,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -290,7 +295,7 @@ export default function MessageBox({ channel }: Props) {
const nonce = ulid(); const nonce = ulid();
try { try {
await client.channels.sendMessage(channel._id, { await channel.sendMessage({
content, content,
nonce, nonce,
replies, replies,
...@@ -325,7 +330,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -325,7 +330,7 @@ export default function MessageBox({ channel }: Props) {
const ws = client.websocket; const ws = client.websocket;
if (ws.connected) { if (ws.connected) {
setTyping(+new Date() + 4000); setTyping(+new Date() + 2500);
ws.send({ ws.send({
type: "BeginTyping", type: "BeginTyping",
channel: channel._id, channel: channel._id,
...@@ -346,9 +351,11 @@ export default function MessageBox({ channel }: Props) { ...@@ -346,9 +351,11 @@ export default function MessageBox({ channel }: Props) {
} }
} }
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ // eslint-disable-next-line
channel._id, const debouncedStopTyping = useCallback(
]); debounce(stopTyping as (...args: unknown[]) => void, 1000),
[channel._id],
);
const { const {
onChange, onChange,
onKeyUp, onKeyUp,
...@@ -360,7 +367,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -360,7 +367,7 @@ export default function MessageBox({ channel }: Props) {
users: { type: "channel", id: channel._id }, users: { type: "channel", id: channel._id },
channels: channels:
channel.channel_type === "TextChannel" channel.channel_type === "TextChannel"
? { server: channel.server } ? { server: channel.server_id! }
: undefined, : undefined,
}); });
...@@ -403,7 +410,7 @@ export default function MessageBox({ channel }: Props) { ...@@ -403,7 +410,7 @@ export default function MessageBox({ channel }: Props) {
setReplies={setReplies} setReplies={setReplies}
/> />
<Base> <Base>
{permissions & ChannelPermission.UploadFiles ? ( {channel.permission & ChannelPermission.UploadFiles ? (
<Action> <Action>
<FileUploader <FileUploader
size={24} size={24}
...@@ -475,14 +482,12 @@ export default function MessageBox({ channel }: Props) { ...@@ -475,14 +482,12 @@ export default function MessageBox({ channel }: Props) {
placeholder={ placeholder={
channel.channel_type === "DirectMessage" channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", { ? translate("app.main.channel.message_who", {
person: client.users.get( person: channel.recipient?.username,
client.channels.getRecipient(channel._id),
)?.username,
}) })
: channel.channel_type === "SavedMessages" : channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved") ? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", { : translate("app.main.channel.message_where", {
channel_name: channel.name, channel_name: channel.name ?? undefined,
}) })
} }
disabled={ disabled={
...@@ -511,4 +516,4 @@ export default function MessageBox({ channel }: Props) { ...@@ -511,4 +516,4 @@ export default function MessageBox({ channel }: Props) {
</Base> </Base>
</> </>
); );
} });
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components"; import styled from "styled-components";
import { attachContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { TextReact } from "../../../lib/i18n"; import { TextReact } from "../../../lib/i18n";
import { User } from "../../../mobx"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import { useData } from "../../../mobx/State";
import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks";
import { MessageObject } from "../../../context/revoltjs/util";
import UserShort from "../user/UserShort"; import UserShort from "../user/UserShort";
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase"; import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
...@@ -37,14 +35,14 @@ type SystemMessageParsed = ...@@ -37,14 +35,14 @@ type SystemMessageParsed =
interface Props { interface Props {
attachContext?: boolean; attachContext?: boolean;
message: MessageObject; message: Message;
highlight?: boolean; highlight?: boolean;
hideInfo?: boolean; hideInfo?: boolean;
} }
export const SystemMessage = observer( export const SystemMessage = observer(
({ attachContext, message, highlight, hideInfo }: Props) => { ({ attachContext, message, highlight, hideInfo }: Props) => {
const store = useData(); const client = useClient();
let data: SystemMessageParsed; let data: SystemMessageParsed;
const content = message.content; const content = message.content;
...@@ -57,8 +55,8 @@ export const SystemMessage = observer( ...@@ -57,8 +55,8 @@ export const SystemMessage = observer(
case "user_remove": case "user_remove":
data = { data = {
type: content.type, type: content.type,
user: store.users.get(content.id)!, user: client.users.get(content.id)!,
by: store.users.get(content.by)!, by: client.users.get(content.by)!,
}; };
break; break;
case "user_joined": case "user_joined":
...@@ -67,21 +65,21 @@ export const SystemMessage = observer( ...@@ -67,21 +65,21 @@ export const SystemMessage = observer(
case "user_banned": case "user_banned":
data = { data = {
type: content.type, type: content.type,
user: store.users.get(content.id)!, user: client.users.get(content.id)!,
}; };
break; break;
case "channel_renamed": case "channel_renamed":
data = { data = {
type: "channel_renamed", type: "channel_renamed",
name: content.name, name: content.name,
by: store.users.get(content.by)!, by: client.users.get(content.by)!,
}; };
break; break;
case "channel_description_changed": case "channel_description_changed":
case "channel_icon_changed": case "channel_icon_changed":
data = { data = {
type: content.type, type: content.type,
by: store.users.get(content.by)!, by: client.users.get(content.by)!,
}; };
break; break;
default: default:
......
import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects"; import { Attachment as AttachmentI } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -13,7 +13,7 @@ import Spoiler from "./Spoiler"; ...@@ -13,7 +13,7 @@ import Spoiler from "./Spoiler";
import TextFile from "./TextFile"; import TextFile from "./TextFile";
interface Props { interface Props {
attachment: AttachmentRJS; attachment: AttachmentI;
hasContent: boolean; hasContent: boolean;
} }
......