1 Commits

Author SHA1 Message Date
Declan Chidlow
87dfca26d7 Add AutoComplete for role completion 2025-06-02 10:47:08 +08:00
30 changed files with 408 additions and 810 deletions

View File

@@ -44,11 +44,9 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v3
with: with:
images: revoltchat/client, ghcr.io/revoltchat/client images: revoltchat/client, ghcr.io/revoltchat/client
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
@@ -67,7 +65,6 @@ jobs:
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,44 +0,0 @@
name: Publish to Harbor
on:
push:
branches:
- "master"
workflow_dispatch:
jobs:
legacy-frontend:
name: Build and push to Harbor
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Harbor
uses: docker/login-action@v3
with:
registry: registry.stoatinternal.com
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
registry.stoatinternal.com/stoat/legacy-frontend:${{ github.sha }}
registry.stoatinternal.com/stoat/legacy-frontend:latest
build-args: |
REVOLT_PUBLIC_URL=https://api.stoat.chat/
REVOLT_SAAS=https://git.stoatinternal.com/brand/assets.git
REVOLT_SAAS_BRANCH=for-legacy-web
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,5 +1,4 @@
# syntax=docker.io/docker/dockerfile:1.7-labs FROM node:16-buster AS builder
FROM --platform=$BUILDPLATFORM node:16-buster AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY . . COPY . .
@@ -8,24 +7,12 @@ COPY .env.build .env
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
RUN yarn build:deps RUN yarn build:deps
# RUN yarn typecheck # lol no # RUN yarn typecheck # lol no
ARG REVOLT_SAAS
ARG REVOLT_SAAS_BRANCH
ENV REVOLT_SAAS=$REVOLT_SAAS
ENV REVOLT_SAAS_BRANCH=$REVOLT_SAAS_BRANCH
RUN yarn build:highmem RUN yarn build:highmem
RUN yarn workspaces focus --production --all RUN yarn workspaces focus --production --all
FROM node:24-alpine FROM node:16-alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=builder /usr/src/app .
ARG REVOLT_PUBLIC_URL=https://api.stoat.chat/
ENV REVOLT_PUBLIC_URL=$REVOLT_PUBLIC_URL
COPY docker/package.json docker/yarn.lock .
RUN yarn install --frozen-lockfile
COPY --from=builder --exclude=package.json --exclude=yarn.lock --exclude=.yarn* --exclude=.git --exclude=external --exclude=node_modules /usr/src/app .
EXPOSE 5000 EXPOSE 5000
CMD [ "yarn", "start:inject" ] CMD [ "yarn", "start:inject" ]

View File

@@ -1,8 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell rec {
buildInputs = [
pkgs.nodejs
pkgs.nodejs.pkgs.yarn
];
}

View File

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 828 B

View File

@@ -1,16 +0,0 @@
{
"name": "inject-and-sirv",
"version": "0.0.1",
"scripts": {
"start:inject": "node scripts/inject.js && sirv dist_injected --port 5000 --cors --single --host"
},
"private": true,
"dependencies": {
"fs-extra": "^11.3.0",
"klaw": "^4.1.0",
"sirv-cli": "^3.0.1"
},
"engines": {
"node": ">=18"
}
}

View File

@@ -1,116 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@polka/url@^1.0.0-next.24":
version "1.0.0-next.29"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
console-clear@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/console-clear/-/console-clear-1.1.1.tgz#995e20cbfbf14dd792b672cde387bd128d674bf7"
integrity sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==
fs-extra@^11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
get-port@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
klaw@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/klaw/-/klaw-4.1.0.tgz#5df608067d8cb62bbfb24374f8e5d956323338f3"
integrity sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==
kleur@^4.1.4:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
local-access@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
mrmime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
sade@^1.6.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
dependencies:
mri "^1.1.0"
semiver@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/semiver/-/semiver-1.1.0.tgz#9c97fb02c21c7ce4fcf1b73e2c7a24324bdddd5f"
integrity sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==
sirv-cli@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/sirv-cli/-/sirv-cli-3.0.1.tgz#9a53e4fa85fdc08d54a76fd76a7c866cd4c3988b"
integrity sha512-ICXaF2u6IQhLZ0EXF6nqUF4YODfSQSt+mGykt4qqO5rY+oIiwdg7B8w2PVDBJlQulaS2a3J8666CUoDoAuCGvg==
dependencies:
console-clear "^1.1.0"
get-port "^5.1.1"
kleur "^4.1.4"
local-access "^1.0.1"
sade "^1.6.0"
semiver "^1.0.0"
sirv "^3.0.0"
tinydate "^1.0.0"
sirv@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.1.tgz#32a844794655b727f9e2867b777e0060fbe07bf3"
integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==
dependencies:
"@polka/url" "^1.0.0-next.24"
mrmime "^2.0.0"
totalist "^3.0.0"
tinydate@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/tinydate/-/tinydate-1.3.0.tgz#e6ca8e5a22b51bb4ea1c3a2a4fd1352dbd4c57fb"
integrity sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==
totalist@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==

2
external/lang vendored

View File

@@ -1,11 +1,11 @@
<!doctype html> <!DOCTYPE html>
<html lang="en" background="#191919"> <html lang="en" background="#191919">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<!--App Title--> <!--App Title-->
<title>Stoat (Legacy)</title> <title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Stoat (Legacy)" /> <meta name="apple-mobile-web-app-title" content="Revolt" />
<!--App Scaling--> <!--App Scaling-->
<meta <meta

View File

@@ -4,9 +4,9 @@ import { Channel } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Button, Checkbox, Preloader } from "@revoltchat/ui"; import { Button, Checkbox } from "@revoltchat/ui";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { SECTION_NSFW } from "../../mobx/stores/Layout"; import { SECTION_NSFW } from "../../mobx/stores/Layout";
@@ -45,36 +45,14 @@ type Props = {
channel: Channel; channel: Channel;
}; };
let geoBlock:
| undefined
| {
countryCode: string;
isAgeRestrictedGeo: true;
};
export default observer((props: Props) => { export default observer((props: Props) => {
const history = useHistory(); const history = useHistory();
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
const [geoLoaded, setGeoLoaded] = useState(typeof geoBlock !== "undefined");
const [ageGate, setAgeGate] = useState(false); const [ageGate, setAgeGate] = useState(false);
useEffect(() => { if (ageGate || !props.gated) {
if (!geoLoaded) {
fetch("https://geo.revolt.chat")
.then((res) => res.json())
.then((data) => {
geoBlock = data;
setGeoLoaded(true);
});
}
}, []);
if (!geoBlock) return <Preloader type="spinner" />;
if ((ageGate && !geoBlock.isAgeRestrictedGeo) || !props.gated) {
return <>{props.children}</>; return <>{props.children}</>;
} }
if ( if (
!( !(
props.channel.channel_type === "Group" || props.channel.channel_type === "Group" ||
@@ -98,40 +76,23 @@ export default observer((props: Props) => {
</a> </a>
</span> </span>
{geoBlock.isAgeRestrictedGeo ? ( <Checkbox
<div style={{ maxWidth: "420px", textAlign: "center" }}> title={<Text id="app.main.channel.nsfw.confirm" />}
{geoBlock.countryCode === "GB" value={layout.getSectionState(SECTION_NSFW, false)}
? "This channel is not available in your region while we review options on legal compliance." onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}
: "This content is not available in your region."} />
</div> <div className="actions">
) : ( <Button palette="secondary" onClick={() => history.goBack()}>
<> <Text id="app.special.modals.actions.back" />
<Checkbox </Button>
title={<Text id="app.main.channel.nsfw.confirm" />} <Button
value={layout.getSectionState(SECTION_NSFW, false)} palette="secondary"
onChange={() => onClick={() =>
layout.toggleSectionState(SECTION_NSFW, false) layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
} }>
/> <Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
<div className="actions"> </Button>
<Button </div>
palette="secondary"
onClick={() => history.goBack()}>
<Text id="app.special.modals.actions.back" />
</Button>
<Button
palette="secondary"
onClick={() =>
layout.getSectionState(SECTION_NSFW) &&
setAgeGate(true)
}>
<Text
id={`app.main.channel.nsfw.${props.type}.confirm`}
/>
</Button>
</div>
</>
)}
</Base> </Base>
); );
}); });

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Channel, User } from "revolt.js"; import { Channel, User, Role } from "revolt.js";
import { Emoji as CustomEmoji } from "revolt.js/esm/maps/Emojis"; import { Emoji as CustomEmoji } from "revolt.js/esm/maps/Emojis";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
@@ -29,11 +29,16 @@ export type AutoCompleteState =
type: "channel"; type: "channel";
matches: Channel[]; matches: Channel[];
} }
| {
type: "role";
matches: Role[];
}
)); ));
export type SearchClues = { export type SearchClues = {
users?: { type: "channel"; id: string } | { type: "all" }; users?: { type: "channel"; id: string } | { type: "all" };
channels?: { server: string }; channels?: { server: string };
roles?: { server: string };
}; };
export type AutoCompleteProps = { export type AutoCompleteProps = {
@@ -59,7 +64,7 @@ export function useAutoComplete(
function findSearchString( function findSearchString(
el: HTMLTextAreaElement, el: HTMLTextAreaElement,
): ["emoji" | "user" | "channel", string, number] | undefined { ): ["emoji" | "user" | "channel" | "role", string, number] | undefined {
if (el.selectionStart === el.selectionEnd) { if (el.selectionStart === el.selectionEnd) {
const cursor = el.selectionStart; const cursor = el.selectionStart;
const content = el.value.slice(0, cursor); const content = el.value.slice(0, cursor);
@@ -71,6 +76,8 @@ export function useAutoComplete(
return ["user", "", j]; return ["user", "", j];
} else if (content[j] === "#") { } else if (content[j] === "#") {
return ["channel", "", j]; return ["channel", "", j];
} else if (content[j] === "%") {
return ["role", "", j];
} }
while (j >= 0 && valid.test(content[j])) { while (j >= 0 && valid.test(content[j])) {
@@ -80,7 +87,12 @@ export function useAutoComplete(
if (j === -1) return; if (j === -1) return;
const current = content[j]; const current = content[j];
if (current === ":" || current === "@" || current === "#") { if (
current === ":" ||
current === "@" ||
current === "#" ||
current === "%"
) {
const search = content.slice(j + 1, content.length); const search = content.slice(j + 1, content.length);
const minLen = current === ":" ? 2 : 1; const minLen = current === ":" ? 2 : 1;
@@ -90,6 +102,8 @@ export function useAutoComplete(
? "channel" ? "channel"
: current === ":" : current === ":"
? "emoji" ? "emoji"
: current === "%"
? "role"
: "user", : "user",
search.toLowerCase(), search.toLowerCase(),
current === ":" ? j + 1 : j, current === ":" ? j + 1 : j,
@@ -230,6 +244,42 @@ export function useAutoComplete(
return; return;
} }
} }
if (type === "role" && searchClues?.roles) {
const server = client.servers.get(searchClues.roles.server);
let roles: (Role & { id: string })[] = [];
if (server?.roles) {
roles = Object.entries(server.roles).map(([id, role]) => ({
...role,
id,
}));
}
const matches = (
search.length > 0
? roles.filter((role) =>
role.name.toLowerCase().match(regex),
)
: roles
)
.splice(0, 5)
.filter((x) => typeof x !== "undefined");
if (matches.length > 0) {
const currentPosition =
state.type !== "none" ? state.selected : 0;
setState({
type: "role",
matches,
selected: Math.min(currentPosition, matches.length - 1),
within: false,
});
return;
}
}
} }
if (state.type !== "none") { if (state.type !== "none") {
@@ -262,6 +312,14 @@ export function useAutoComplete(
state.matches[state.selected]._id, state.matches[state.selected]._id,
"> ", "> ",
); );
} else if (state.type === "role") {
content.splice(
index,
search.length + 1,
"<%",
state.matches[state.selected].id,
"> ",
);
} else { } else {
content.splice( content.splice(
index, index,
@@ -492,7 +550,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} key={match._id}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
@@ -517,7 +575,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} key={match._id}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
@@ -539,6 +597,40 @@ export default function AutoComplete({
{match.name} {match.name}
</button> </button>
))} ))}
{state.type === "role" &&
state.matches.map((match, i) => (
<button
key={match._id}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
setState({
...state,
selected: i,
within: true,
})
}
onMouseLeave={() =>
state.within &&
setState({
...state,
within: false,
})
}
onClick={onClick}>
<div
style={{
width: "16px",
height: "16px",
borderRadius: "50%",
backgroundColor: match.colour || "#7c7c7c",
marginRight: "8px",
flexShrink: 0,
}}
/>
{match.name}
</button>
))}
</div> </div>
</Base> </Base>
); );

View File

@@ -567,6 +567,7 @@ export default observer(({ channel }: Props) => {
channel.channel_type === "TextChannel" channel.channel_type === "TextChannel"
? { server: channel.server_id! } ? { server: channel.server_id! }
: undefined, : undefined,
roles: { server: channel.server_id! },
}); });
return ( return (

View File

@@ -26,8 +26,8 @@ export default function AttachmentActions({ attachment }: Props) {
const { filename, metadata, size } = attachment; const { filename, metadata, size } = attachment;
const url = client.generateFileURL(attachment); const url = client.generateFileURL(attachment);
const open_url = url; const open_url = `${url}/${filename}`;
const download_url = `${url}/${filename}`; const download_url = url;
const filesize = determineFileSize(size); const filesize = determineFileSize(size);

View File

@@ -126,16 +126,8 @@ export function EmbedInvite({ code }: Props) {
<EmbedInviteName>{invite.server_name}</EmbedInviteName> <EmbedInviteName>{invite.server_name}</EmbedInviteName>
<EmbedInviteMemberCount> <EmbedInviteMemberCount>
<Group size={12} /> <Group size={12} />
{invite.member_count != null ? ( {invite.member_count.toLocaleString()}{" "}
<> {invite.member_count === 1 ? "member" : "members"}
{invite.member_count.toLocaleString()}{" "}
{invite.member_count === 1
? "member"
: "members"}
</>
) : (
"N/A"
)}
</EmbedInviteMemberCount> </EmbedInviteMemberCount>
</EmbedInviteDetails> </EmbedInviteDetails>
{processing ? ( {processing ? (

View File

@@ -119,7 +119,7 @@ export default function UserBadges({ badges, uid }: Props) {
}} }}
onClick={() => { onClick={() => {
window.open( window.open(
"https://ko-fi.com/stoatchat", "https://wiki.revolt.chat/notes/project/financial-support/",
"_blank", "_blank",
); );
}} }}

View File

@@ -34,7 +34,7 @@ export function RenderEmoji({ match }: CustomComponentProps) {
? `${ ? `${
clientController.getAvailableClient().configuration?.features clientController.getAvailableClient().configuration?.features
.autumn.url .autumn.url
}/emojis/${match}/original` }/emojis/${match}`
: parseEmoji( : parseEmoji(
match in emojiDictionary match in emojiDictionary
? emojiDictionary[match as keyof typeof emojiDictionary] ? emojiDictionary[match as keyof typeof emojiDictionary]

View File

@@ -50,10 +50,8 @@ const ServerList = styled.div`
export default observer(() => { export default observer(() => {
const client = useClient(); const client = useClient();
const state = useApplicationState(); const state = useApplicationState();
const { server: server_id, channel: channel_id } = useParams<{ const { server: server_id, channel: channel_id } =
server: string; useParams<{ server: string; channel?: string }>();
channel?: string;
}>();
const server = client.servers.get(server_id); const server = client.servers.get(server_id);
if (!server) return <Redirect to="/" />; if (!server) return <Redirect to="/" />;
@@ -121,18 +119,14 @@ export default observer(() => {
channels.push(addChannel(id)); channels.push(addChannel(id));
} }
if (category.title === "Default") { elements.push(
elements.push(...channels); <CollapsibleSection
} else { id={`category_${category.id}`}
elements.push( defaultValue
<CollapsibleSection summary={<Category>{category.title}</Category>}>
id={`category_${category.id}`} {channels}
defaultValue </CollapsibleSection>,
summary={<Category>{category.title}</Category>}> );
{channels}
</CollapsibleSection>,
);
}
} }
} }

View File

@@ -121,7 +121,7 @@ export default function MemberList({
Offline users have temporarily been disabled for Offline users have temporarily been disabled for
larger servers - see{" "} larger servers - see{" "}
<a <a
href="https://github.com/stoatchat/stoatchat/issues/178" href="https://github.com/revoltchat/backend/issues/178"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
issue #178 issue #178

View File

@@ -76,25 +76,17 @@ function useEntries(
keys.forEach((key) => { keys.forEach((key) => {
let u; let u;
let member;
if (isServer) { if (isServer) {
const { server, user } = JSON.parse(key); const { server, user } = JSON.parse(key);
if (server !== channel.server_id) return; if (server !== channel.server_id) return;
u = client.users.get(user); u = client.users.get(user);
member = client.members.get(key);
if (!member?.hasPermission(channel, "ViewChannel")) {
return;
}
} else { } else {
u = client.users.get(key); u = client.users.get(key);
member = client.members.get(key);
} }
if (!u) return; if (!u) return;
const member = client.members.get(key);
const sort = member?.nickname ?? u.username; const sort = member?.nickname ?? u.username;
const entry = [u, sort] as [User, string]; const entry = [u, sort] as [User, string];

View File

@@ -78,7 +78,6 @@ export type Theme = Overrides & {
css?: string; css?: string;
monospaceFont?: MonospaceFonts; monospaceFont?: MonospaceFonts;
"min-opacity"?: number; "min-opacity"?: number;
"logo-filter"?: string;
}; };
export type ComputedVariables = Theme & { export type ComputedVariables = Theme & {
@@ -289,7 +288,6 @@ export const PRESETS: Record<string, Theme> = {
"status-busy": "#F84848", "status-busy": "#F84848",
"status-streaming": "#977EFF", "status-streaming": "#977EFF",
"status-invisible": "#A5A5A5", "status-invisible": "#A5A5A5",
"logo-filter": "unset",
}, },
dark: { dark: {
accent: "#FD6671", accent: "#FD6671",
@@ -318,7 +316,6 @@ export const PRESETS: Record<string, Theme> = {
"status-busy": "#F84848", "status-busy": "#F84848",
"status-streaming": "#977EFF", "status-streaming": "#977EFF",
"status-invisible": "#A5A5A5", "status-invisible": "#A5A5A5",
"logo-filter": "invert(1)",
}, },
}; };

View File

@@ -66,16 +66,11 @@ export default class Settings
} }
@action hydrate(data: ISettings) { @action hydrate(data: ISettings) {
Object.keys(data).forEach((key) => { Object.keys(data).forEach(
const val = (data as any)[key]; (key) =>
if (typeof val !== "undefined") { typeof (data as any)[key] !== "undefined" &&
if (key === "appearance:emoji") { this.data.set(key, (data as any)[key]),
setGlobalEmojiPack(val); );
}
this.data.set(key, val);
}
});
} }
/** /**

View File

@@ -107,10 +107,7 @@ export default observer(() => {
<h3> <h3>
<Text id="app.special.modals.onboarding.welcome" /> <Text id="app.special.modals.onboarding.welcome" />
<br /> <br />
<img <img src={wideSVG} />
src={wideSVG}
style={{ filter: "var(--logo-filter)" }}
/>
</h3> </h3>
<div className={styles.actions}> <div className={styles.actions}>
<a <a
@@ -167,24 +164,18 @@ export default observer(() => {
</Link> </Link>
)} )}
<a <Link to="/settings/feedback">
href={
typeof window.native === "object"
? "https://stoat.chat/download"
: "https://stoat.chat/app"
}
target="_blank">
<CategoryButton <CategoryButton
action="chevron" action="chevron"
icon={<Megaphone size={32} />} icon={<Megaphone size={32} />}
description="Revolt is now Stoat and has a brand new app."> description={
{typeof window.native === "object" <Text id="app.home.feedback_desc" />
? "Download new app" }>
: "Switch to new app"} <Text id="app.home.feedback" />
</CategoryButton> </CategoryButton>
</a> </Link>
<a <a
href="https://ko-fi.com/stoatchat" href="https://wiki.revolt.chat/notes/project/financial-support/"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<CategoryButton <CategoryButton

View File

@@ -10,22 +10,6 @@ import UserIcon from "../../components/common/user/UserIcon";
import Markdown from "../../components/markdown/Markdown"; import Markdown from "../../components/markdown/Markdown";
import { useClient } from "../../controllers/client/ClientController"; import { useClient } from "../../controllers/client/ClientController";
const Page = styled.div`
padding: 6em;
min-height: 100vh;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
@media (max-width: 768px) {
padding: 2em;
}
@media (max-width: 480px) {
padding: 1em;
}
`;
const BotInfo = styled.div` const BotInfo = styled.div`
gap: 12px; gap: 12px;
display: flex; display: flex;
@@ -58,7 +42,7 @@ export default function InviteBot() {
const [group, setGroup] = useState("none"); const [group, setGroup] = useState("none");
return ( return (
<Page> <div style={{ padding: "6em" }}>
<Tip palette="warning">This section is under construction.</Tip> <Tip palette="warning">This section is under construction.</Tip>
{typeof data === "undefined" && <Preloader type="spinner" />} {typeof data === "undefined" && <Preloader type="spinner" />}
{data && ( {data && (
@@ -122,7 +106,6 @@ export default function InviteBot() {
</Option> </Option>
</> </>
)} )}
</Page> </div>
); );
} }

View File

@@ -41,9 +41,7 @@ export default observer(() => {
href={action.href} href={action.href}
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<div className="button"> <div className="button">{action.text}</div>{" "}
{action.text}
</div>{" "}
</a> </a>
) : null, ) : null,
)} )}
@@ -61,11 +59,7 @@ export default observer(() => {
<div className={styles.nav}> <div className={styles.nav}>
<a className={styles.logo}> <a className={styles.logo}>
{!("native" in window) && ( {!("native" in window) && (
<img <img src={wideSVG} draggable={false} />
src={wideSVG}
style={{ filter: "var(--logo-filter)" }}
draggable={false}
/>
)} )}
</a> </a>
<LocaleSelector /> <LocaleSelector />

View File

@@ -141,10 +141,6 @@
color: goldenrod !important; color: goldenrod !important;
} }
.newApp {
color: green !important;
}
.logOut, .logOut,
.deleteServer { .deleteServer {
color: var(--error) !important; color: var(--error) !important;

View File

@@ -207,6 +207,11 @@ export default observer(() => {
icon: <Bot size={20} />, icon: <Bot size={20} />,
title: <Text id="app.settings.pages.bots.title" />, title: <Text id="app.settings.pages.bots.title" />,
}, },
{
id: "feedback",
icon: <Megaphone size={20} />,
title: <Text id="app.settings.pages.feedback.title" />,
},
]} ]}
children={ children={
<Switch> <Switch>
@@ -258,23 +263,16 @@ export default observer(() => {
category="pages" category="pages"
custom={ custom={
<> <>
{typeof window.native === "object" ? ( <ButtonItem
<a compact
className={styles.newApp} onClick={() =>
href="https://stoat.chat/download" modalController.push({ type: "changelog" })
target="_blank"> }>
<ButtonItem compact>Download new app</ButtonItem> <ListUl size={20} />
</a> <Text id="app.special.modals.changelogs.title" />
) : ( </ButtonItem>
<a
className={styles.newApp}
href="https://stoat.chat/app"
target="_blank">
<ButtonItem compact>Switch to new app</ButtonItem>
</a>
)}
<a <a
href="https://github.com/stoatchat" href="https://github.com/revoltchat"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<ButtonItem compact> <ButtonItem compact>
@@ -283,7 +281,7 @@ export default observer(() => {
</ButtonItem> </ButtonItem>
</a> </a>
<a <a
href="https://ko-fi.com/stoatchat" href="https://wiki.revolt.chat/notes/project/financial-support/"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<ButtonItem className={styles.donate} compact> <ButtonItem className={styles.donate} compact>
@@ -311,7 +309,7 @@ export default observer(() => {
<a <a
href={ href={
GIT_BRANCH !== "DETACHED" GIT_BRANCH !== "DETACHED"
? `https://github.com/stoatchat/for-legacy-web/tree/${GIT_BRANCH}` ? `https://github.com/revoltchat/revite/tree/${GIT_BRANCH}`
: undefined : undefined
} }
target="_blank" target="_blank"

View File

@@ -33,8 +33,9 @@ export function Audio() {
const askOrGetPermission = async () => { const askOrGetPermission = async () => {
try { try {
const result = const result = await navigator.mediaDevices.getUserMedia(
await navigator.mediaDevices.getUserMedia(constraints); constraints,
);
setMediaStream(result); setMediaStream(result);
} catch (err) { } catch (err) {
@@ -95,14 +96,28 @@ export function Audio() {
<div className={styles.audio}> <div className={styles.audio}>
<Tip palette="warning"> <Tip palette="warning">
<span> <span>
Legacy voice is no longer supported,{" "} We are currently{" "}
<a <a
style={{ color: "inherit", fontWeight: "600" }} style={{ color: "inherit", fontWeight: "600" }}
href="https://stoat.chat/app" href="https://github.com/revoltchat/frontend/issues/14"
target="_blank"> target="_blank"
use the new app rel="noreferrer">
</a> rebuilding the client
. </a>{" "}
and{" "}
<a
style={{ color: "inherit", fontWeight: "600" }}
href="https://trello.com/c/Ay6KdiOV/1-voice-overhaul-and-video-calling"
target="_blank"
rel="noreferrer">
the voice server
</a>{" "}
from scratch.
<br />
<br />
The old voice should work in most cases, but it may
inexplicably not connect in some scenarios and / or
exhibit weird behaviour.
</span> </span>
</Tip> </Tip>

View File

@@ -1,8 +1,4 @@
import { import { HelpCircle } from "@styled-icons/boxicons-solid";
HelpCircle,
ChevronUp,
ChevronDown,
} from "@styled-icons/boxicons-solid";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Server } from "revolt.js"; import { Server } from "revolt.js";
@@ -20,208 +16,17 @@ import {
ColourSwatches, ColourSwatches,
InputBox, InputBox,
Category, Category,
Row,
} from "@revoltchat/ui"; } from "@revoltchat/ui";
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import { PermissionList } from "../../../components/settings/roles/PermissionList"; import { PermissionList } from "../../../components/settings/roles/PermissionList";
import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection"; import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection";
import { useSession } from "../../../controllers/client/ClientController";
import { modalController } from "../../../controllers/modals/ModalController"; import { modalController } from "../../../controllers/modals/ModalController";
interface Props { interface Props {
server: Server; server: Server;
} }
const RoleReorderContainer = styled.div`
margin: 16px 0;
`;
const RoleItem = styled.div`
display: flex;
align-items: center;
padding: 12px 16px;
margin: 12px 0;
background: var(--secondary-background);
border-radius: var(--border-radius);
`;
const RoleInfo = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`;
const RoleName = styled.div`
font-weight: 600;
color: var(--foreground);
`;
const RoleRank = styled.div`
font-size: 12px;
color: var(--secondary-foreground);
`;
const RoleControls = styled.div`
display: flex;
gap: 4px;
`;
const MoveButton = styled(Button)`
padding: 4px 8px;
min-width: auto;
`;
/**
* Hook to memo-ize role information with proper ordering
* @param server Target server
* @returns Role array with default at bottom
*/
export function useRolesForReorder(server: Server) {
return useMemo(() => {
const roles = [...server.orderedRoles] as RoleOrDefault[];
roles.push({
id: "default",
name: "Default",
permissions: server.default_permissions,
});
return roles;
}, [server.roles, server.default_permissions]);
}
/**
* Role reordering component
*/
const RoleReorderPanel = observer(
({ server, onExit }: Props & { onExit: () => void }) => {
const initialRoles = useRolesForReorder(server);
const [roles, setRoles] = useState(initialRoles);
const [isReordering, setIsReordering] = useState(false);
// Update local state when server roles change
useMemo(() => {
setRoles(useRolesForReorder(server));
}, [server.roles, server.default_permissions]);
const moveRoleUp = (index: number) => {
if (index === 0 || roles[index].id === "default") return;
const newRoles = [...roles];
[newRoles[index - 1], newRoles[index]] = [
newRoles[index],
newRoles[index - 1],
];
setRoles(newRoles);
};
const moveRoleDown = (index: number) => {
// Can't move down if it's the last non-default role or if it's default
if (index >= roles.length - 2 || roles[index].id === "default")
return;
const newRoles = [...roles];
[newRoles[index], newRoles[index + 1]] = [
newRoles[index + 1],
newRoles[index],
];
setRoles(newRoles);
};
const saveReorder = async () => {
setIsReordering(true);
try {
const nonDefaultRoles = roles.filter(
(role) => role.id !== "default",
);
const roleIds = nonDefaultRoles.map((role) => role.id);
const session = useSession()!;
const client = session.client!;
// Make direct API request since it's not in r.js as of writing
await client.api.patch(`/servers/${server._id}/roles/ranks`, {
ranks: roleIds,
});
console.log("Roles reordered successfully");
} catch (error) {
console.error("Failed to reorder roles:", error);
setRoles(initialRoles);
} finally {
setIsReordering(false);
}
};
const hasChanges = !isEqual(
roles.filter((r) => r.id !== "default").map((r) => r.id),
initialRoles.filter((r) => r.id !== "default").map((r) => r.id),
);
return (
<div>
<SpaceBetween>
<H1>
<Text id="app.settings.permissions.role_ranking" />
</H1>
<Row>
<Button
palette="secondary"
onClick={onExit}
style={{ marginBottom: "16px" }}>
<Text id="app.special.modals.actions.back" />
</Button>
<Button
palette="secondary"
disabled={!hasChanges || isReordering}
onClick={saveReorder}>
<Text id="app.special.modals.actions.save" />
</Button>
</Row>
</SpaceBetween>
<RoleReorderContainer>
{roles.map((role, index) => (
<RoleItem key={role.id}>
<RoleInfo>
<RoleName>{role.name}</RoleName>
<RoleRank>
{role.id === "default" ? (
<Text id="app.settings.permissions.default_desc" />
) : (
<>
<Text id="app.settings.permissions.role_ranking" />{" "}
{index}
</>
)}
</RoleRank>
</RoleInfo>
{role.id !== "default" && (
<RoleControls>
<MoveButton
palette="secondary"
disabled={index === 0}
onClick={() => moveRoleUp(index)}>
<ChevronUp size={16} />
</MoveButton>
<MoveButton
palette="secondary"
disabled={index >= roles.length - 2}
onClick={() => moveRoleDown(index)}>
<ChevronDown size={16} />
</MoveButton>
</RoleControls>
)}
</RoleItem>
))}
</RoleReorderContainer>
</div>
);
},
);
/** /**
* Hook to memo-ize role information. * Hook to memo-ize role information.
* @param server Target server * @param server Target server
@@ -245,11 +50,9 @@ export function useRoles(server: Server) {
} }
/** /**
* Updated Roles settings menu with reordering panel * Roles settings menu
*/ */
export const Roles = observer(({ server }: Props) => { export const Roles = observer(({ server }: Props) => {
const [showReorderPanel, setShowReorderPanel] = useState(false);
// Consolidate all permissions that we can change right now. // Consolidate all permissions that we can change right now.
const currentRoles = useRoles(server); const currentRoles = useRoles(server);
@@ -271,219 +74,213 @@ export const Roles = observer(({ server }: Props) => {
margin: 16px 0; margin: 16px 0;
`; `;
const ReorderButton = styled(Button)`
margin-inline: auto 8px;
`;
if (showReorderPanel) {
return (
<div>
<RoleReorderPanel
server={server}
onExit={() => setShowReorderPanel(false)}
/>
</div>
);
}
return ( return (
<div> <PermissionsLayout
<PermissionsLayout server={server}
server={server} rank={server.member?.ranking ?? Infinity}
rank={server.member?.ranking ?? Infinity} onCreateRole={(callback) =>
onCreateRole={(callback) => modalController.push({
modalController.push({ type: "create_role",
type: "create_role", server,
server, callback,
callback, })
}) }
editor={({ selected }) => {
const currentRole = currentRoles.find(
(x) => x.id === selected,
)!;
if (!currentRole) return null;
// Keep track of whatever role we're editing right now.
const [value, setValue] = useState<Partial<RoleOrDefault>>({});
const currentRoleValue = { ...currentRole, ...value };
// Calculate permissions we have access to on this server.
const current = server.permission;
// Upload new role information to server.
function save() {
const { permissions: permsCurrent, ...current } =
currentRole;
const { permissions: permsValue, ...value } =
currentRoleValue;
if (!isEqual(permsCurrent, permsValue)) {
server.setPermissions(
selected,
typeof permsValue === "number"
? permsValue
: {
allow: permsValue.a,
deny: permsValue.d,
},
);
}
if (!isEqual(current, value)) {
server.editRole(selected, value);
}
} }
editor={({ selected }) => {
const currentRole = currentRoles.find(
(x) => x.id === selected,
)!;
if (!currentRole) return null; // Delete the role from this server.
function deleteRole() {
server.deleteRole(selected);
}
const [value, setValue] = useState<Partial<RoleOrDefault>>( return (
{}, <div>
); <SpaceBetween>
<H1>
const currentRoleValue = { ...currentRole, ...value }; <Text
id="app.settings.actions.edit"
function save() { fields={{ name: currentRole.name }}
const { permissions: permsCurrent, ...current } = />
currentRole; </H1>
const { permissions: permsValue, ...value } = <Button
currentRoleValue; palette="secondary"
disabled={isEqual(
if (!isEqual(permsCurrent, permsValue)) { currentRole,
server.setPermissions( currentRoleValue,
selected, )}
typeof permsValue === "number" onClick={save}>
? permsValue <Text id="app.special.modals.actions.save" />
: { </Button>
allow: permsValue.a, </SpaceBetween>
deny: permsValue.d, <hr />
}, {selected !== "default" && (
); <>
} <section>
<Category>
if (!isEqual(current, value)) { <Text id="app.settings.permissions.role_name" />
server.editRole(selected, value); </Category>
} <p>
} <InputBox
value={currentRoleValue.name}
function deleteRole() { onChange={(e) =>
server.deleteRole(selected); setValue({
} ...value,
name: e.currentTarget.value,
return ( })
<div> }
<SpaceBetween> palette="secondary"
<H1> />
<Text </p>
id="app.settings.actions.edit" </section>
fields={{ name: currentRole.name }} <section>
/> <Category>{"Role ID"}</Category>
</H1> <RoleId>
<ReorderButton <Tooltip
palette="secondary" content={
onClick={() => setShowReorderPanel(true)}> "This is a unique identifier for this role."
<Text id="app.settings.permissions.role_ranking" /> }>
</ReorderButton> <HelpCircle size={16} />
<Button </Tooltip>
palette="secondary" <Tooltip
disabled={isEqual( content={
currentRole, <Text id="app.special.copy" />
currentRoleValue, }>
)} <a
onClick={save}> onClick={() =>
<Text id="app.special.modals.actions.save" /> modalController.writeText(
</Button> currentRole.id,
</SpaceBetween> )
<hr />
{selected !== "default" && (
<>
<section>
<Category>
<Text id="app.settings.permissions.role_name" />
</Category>
<p>
<InputBox
value={currentRoleValue.name}
onChange={(e) =>
setValue({
...value,
name: e.currentTarget
.value,
})
}
palette="secondary"
/>
</p>
</section>
<section>
<Category>{"Role ID"}</Category>
<RoleId>
<Tooltip
content={
"This is a unique identifier for this role."
}> }>
<HelpCircle size={16} /> {currentRole.id}
</Tooltip> </a>
<Tooltip </Tooltip>
content={ </RoleId>
<Text id="app.special.copy" /> </section>
}> <section>
<a <Category>
onClick={() => <Text id="app.settings.permissions.role_colour" />
modalController.writeText( </Category>
currentRole.id, <p>
) <ColourSwatches
}> value={
{currentRole.id} currentRoleValue.colour ??
</a> "gray"
</Tooltip> }
</RoleId> onChange={(colour) =>
</section> setValue({ ...value, colour })
<section> }
<Category> />
<Text id="app.settings.permissions.role_colour" /> </p>
</Category> </section>
<p> <section>
<ColourSwatches <Category>
value={ <Text id="app.settings.permissions.role_options" />
currentRoleValue.colour ?? </Category>
"gray" <p>
} <Checkbox
onChange={(colour) => value={
setValue({ currentRoleValue.hoist ?? false
...value, }
colour, onChange={(hoist) =>
}) setValue({ ...value, hoist })
} }
/> title={
</p> <Text id="app.settings.permissions.hoist_role" />
</section> }
<section> description={
<Category> <Text id="app.settings.permissions.hoist_desc" />
<Text id="app.settings.permissions.role_options" /> }
</Category> />
<p> </p>
<Checkbox </section>
value={ <section>
currentRoleValue.hoist ?? <Category>
false <Text id="app.settings.permissions.role_ranking" />
} </Category>
onChange={(hoist) => <p>
setValue({ <InputBox
...value, type="number"
hoist, value={currentRoleValue.rank ?? 0}
}) onChange={(e) =>
} setValue({
title={ ...value,
<Text id="app.settings.permissions.hoist_role" /> rank: parseInt(
} e.currentTarget.value,
description={ ),
<Text id="app.settings.permissions.hoist_desc" /> })
} }
/> palette="secondary"
</p> />
</section> </p>
</> </section>
)} </>
<h1> )}
<Text id="app.settings.permissions.edit_title" /> <h1>
</h1> <Text id="app.settings.permissions.edit_title" />
<PermissionList </h1>
value={currentRoleValue.permissions} <PermissionList
onChange={(permissions) => value={currentRoleValue.permissions}
setValue({ onChange={(permissions) =>
...value, setValue({
permissions, ...value,
} as RoleOrDefault) permissions,
} } as RoleOrDefault)
target={server} }
/> target={server}
{selected !== "default" && ( />
<> {selected !== "default" && (
<hr /> <>
<h1> <hr />
<Text id="app.settings.categories.danger_zone" /> <h1>
</h1> <Text id="app.settings.categories.danger_zone" />
<DeleteRoleButton </h1>
palette="error" <DeleteRoleButton
compact palette="error"
onClick={deleteRole}> compact
<Text id="app.settings.permissions.delete_role" /> onClick={deleteRole}>
</DeleteRoleButton> <Text id="app.settings.permissions.delete_role" />
</> </DeleteRoleButton>
)} </>
</div> )}
); </div>
}} );
/> }}
</div> />
); );
}); });

View File

@@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
// Strings needs to be explictly stated here as they can cause type issues elsewhere. // Strings needs to be explictly stated here as they can cause type issues elsewhere.
export const REPO_URL: string = "https://github.com/stoatchat/for-legacy-web/commit"; export const REPO_URL: string = "https://github.com/revoltchat/revite/commit";
export const GIT_REVISION: string = "__GIT_REVISION__"; export const GIT_REVISION: string = "__GIT_REVISION__";
export const GIT_BRANCH: string = "__GIT_BRANCH__"; export const GIT_BRANCH: string = "__GIT_BRANCH__";