2 Commits

Author SHA1 Message Date
Jan
0f0d41615b fix: properly align emoji name in autocompletion 2022-10-26 09:13:14 +02:00
Jan
35fb6b4381 feat: list custom emojis in autocomplete 2022-10-26 08:22:39 +02:00
98 changed files with 810 additions and 2415 deletions

5
.env
View File

@@ -1,5 +1,2 @@
# VITE_API_URL=https://api.revolt.chat VITE_API_URL=https://api.revolt.chat
# VITE_API_URL=https://app.revolt.chat/api
# VITE_API_URL=http://local.revolt.chat:8000
VITE_API_URL=https://app.revolt.chat/api
VITE_THEMES_URL=https://themes.revolt.chat VITE_THEMES_URL=https://themes.revolt.chat

View File

@@ -1,6 +1,6 @@
## Please make sure to check the following tasks before opening and submitting a PR ## Please make sure to check the following tasks before opening and submitting a PR
* [ ] I understand and have followed the [contribution guide](https://developers.revolt.chat/contrib.html) * [ ] I understand and have followed the [contribution guide](https://github.com/revoltchat/revolt/discussions/282)
* [ ] I have tested my changes locally and they are working as intended * [ ] I have tested my changes locally and they are working as intended
* [ ] These changes do not have any notable side effects on other Revolt projects * [ ] These changes do not have any notable side effects on other Revolt projects
* [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations) * [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations)

View File

@@ -30,44 +30,82 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/${{ matrix.architecture }}
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
- name: Build
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ matrix.architecture }}
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
publish: publish:
needs: [test]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: "recursive"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v1
- name: Cache amd64 Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/linux/amd64
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
- 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'
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry - name: Login to Github Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish - name: Build and publish
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: true
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 }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

16
.github/workflows/mirroring.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Mirroring
on:
push:
branches:
- "master"
jobs:
to_gitlab:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitlab.com:insert/revolt-vite.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}

View File

@@ -15,27 +15,22 @@ jobs:
gh api graphql -f query=' gh api graphql -f query='
query { query {
organization(login: "revoltchat"){ organization(login: "revoltchat"){
projectV2(number: 3) { projectNext(number: 3) {
id id
fields(first:20) { fields(first:20) {
nodes { nodes {
... on ProjectV2SingleSelectField { id
id name
name settings
options {
id
name
}
}
} }
} }
} }
} }
}' > project_data.json }' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project - name: Add issue to project
env: env:
@@ -44,11 +39,11 @@ jobs:
run: | run: |
item_id="$( gh api graphql -f query=' item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) { mutation($project:ID!, $issue:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
item { projectNextItem {
id id
} }
} }
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@@ -15,27 +15,22 @@ jobs:
gh api graphql -f query=' gh api graphql -f query='
query { query {
organization(login: "revoltchat"){ organization(login: "revoltchat"){
projectV2(number: 5) { projectNext(number: 3) {
id id
fields(first:20) { fields(first:20) {
nodes { nodes {
... on ProjectV2SingleSelectField { id
id name
name settings
options {
id
name
}
}
} }
} }
} }
} }
}' > project_data.json }' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project - name: Add PR to project
env: env:
@@ -44,12 +39,12 @@ jobs:
run: | run: |
item_id="$( gh api graphql -f query=' item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) { mutation($project:ID!, $pr:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
item { projectNextItem {
id id
} }
} }
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV echo 'ITEM_ID='$item_id >> $GITHUB_ENV
@@ -64,16 +59,14 @@ jobs:
$status_field: ID! $status_field: ID!
$status_value: String! $status_value: String!
) { ) {
set_status: updateProjectV2ItemFieldValue(input: { set_status: updateProjectNextItemField(input: {
projectId: $project projectId: $project
itemId: $item itemId: $item
fieldId: $status_field fieldId: $status_field
value: { value: $status_value
singleSelectOptionId: $status_value
}
}) { }) {
projectV2Item { projectNextItem {
id id
} }
} }
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

View File

@@ -1,21 +1,17 @@
# 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 . .
COPY .env.build .env COPY .env.build .env
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
RUN yarn build:deps RUN yarn typecheck
# RUN yarn typecheck # lol no
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 docker/package.json docker/yarn.lock . COPY --from=builder /usr/src/app .
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,30 +1,3 @@
# Deprecation Notice
This project is deprecated, however it still may receive maintenance updates.
PRs for small fixes are more than welcome.
## Deploying a new release
Ensure `.env.local` points to `https://app.revolt.chat/api`.
```bash
cd ~/deployments/revite
git pull
git submodule update
# check:
git status
export REVOLT_SAAS_BRANCH=revite/main
export REMOTE=root@production
scripts/publish.sh
# SSH in and restart revite:
ssh $REMOTE
tmux a -t 4
```
# Revite # Revite
## Description ## Description

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

@@ -44,10 +44,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@revoltchat/rehype-katex": "6.0.3-patch.1",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"lottie-react": "^2.4.0",
"sirv-cli": "^1.0.14", "sirv-cli": "^1.0.14",
"vite": "^3.0.5" "vite": "^3.0.5"
}, },
@@ -138,6 +136,7 @@
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"react-virtuoso": "^2.12.0", "react-virtuoso": "^2.12.0",
"rehype-katex": "^6.0.2",
"rehype-prism": "^2.1.3", "rehype-prism": "^2.1.3",
"rehype-react": "^7.1.1", "rehype-react": "^7.1.1",
"remark-breaks": "^3.0.2", "remark-breaks": "^3.0.2",

1
packages/components Submodule

Submodule packages/components added at d314b2d191

Submodule packages/hast-util-table-cell-style added at 7803fa5441

1
packages/revolt.js Submodule

Submodule packages/revolt.js added at 39d1f596e2

View File

@@ -2,16 +2,13 @@
# Build and publish release to production server # Build and publish release to production server
# Remote Server # Remote Server
if [ -z "$REMOTE" ]; then REMOTE=revolt-de-nrb-1
echo "Please set REMOTE!"
exit
fi
# Remote Directory # Remote Directory
REMOTE_DIR=/root/deployments/revite REMOTE_DIR=/root/revite
# Post-install script # Post-install script
POST_INSTALL="" POST_INSTALL="pm2 restart revite"
# Assets # Assets
export REVOLT_SAAS=https://github.com/revoltchat/assets export REVOLT_SAAS=https://github.com/revoltchat/assets
@@ -21,7 +18,7 @@ export REVOLT_SAAS=https://github.com/revoltchat/assets
set -e set -e
# 1. Build Revite # 1. Build Revite
yarn build:highmem yarn build
# 2. Archive built files # 2. Archive built files
tar -czvf build.tar.gz dist tar -czvf build.tar.gz dist

38
src/assets/changelogs.ts Normal file
View File

@@ -0,0 +1,38 @@
type Element =
| string
| {
type: "image";
src: string;
};
export interface ChangelogPost {
date: Date;
title: string;
content: Element[];
}
export const changelogEntries: Record<number, ChangelogPost> = {
1: {
date: new Date("2022-06-12T20:39:16.674Z"),
title: "Secure your account with 2FA",
content: [
"Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.",
{
type: "image",
src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png",
},
"Once enabled, you will be prompted on login.",
{
type: "image",
src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png",
},
"Other authentication methods coming later, stay tuned!",
],
},
};
export const changelogEntryArray = Object.keys(changelogEntries).map(
(index) => changelogEntries[index as unknown as number],
);
export const latestChangelog = changelogEntryArray.length;

View File

@@ -1,86 +0,0 @@
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import { JSX } from "preact";
import usernameAnim from "../controllers/modals/components/legacy/usernameUpdateLottie.json";
type Element =
| string
| {
type: "image";
src: string;
shadow?: boolean;
}
| { type: "element"; element: JSX.Element };
export interface ChangelogPost {
date: Date;
title: string;
content: Element[];
}
export const changelogEntries: Record<number, ChangelogPost> = {
1: {
date: new Date("2022-06-12T20:39:16.674Z"),
title: "Secure your account with 2FA",
content: [
"Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.",
{
type: "image",
src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png",
},
"Once enabled, you will be prompted on login.",
{
type: "image",
src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png",
},
"Other authentication methods coming later, stay tuned!",
],
},
2: {
date: new Date("2023-02-23T20:00:00.000Z"),
title: "In-App Reporting Is Here",
content: [
"You can now report any user, server, or message directly from the app.",
{
type: "image",
src: "https://autumn.revolt.chat/attachments/ZuDVIjGiCl61Pk9XGk5qfc8-idN9EnFAk55DUQp713/the.png",
shadow: true,
},
"If you want to learn more about how we're making Revolt safer for you, check out our new blog post :point_right: [https://revolt.chat/posts/improving-user-safety](https://revolt.chat/posts/improving-user-safety)",
],
},
3: {
date: new Date("2023-06-11T15:00:00.000Z"),
title: "Usernames are Changing",
content: [
{
type: "element",
element: (
<Lottie
animationData={usernameAnim}
style={{
background: "var(--secondary-background)",
borderRadius: "6px",
}}
/>
),
},
"Revolt has undergone a significant change to its username system, transitioning from unique username handles to a new system of display names and usernames with four-digit number tags called discriminators. The four-digit number tags serve as identifiers to differentiate users with the same username, allowing individuals to select desired usernames that reflect their identity.",
{
type: "element",
element: (
<a href="https://revolt.chat/posts/evolving-usernames">
Read more on our blog!
</a>
),
},
],
},
};
export const changelogEntryArray = Object.keys(changelogEntries).map(
(index) => changelogEntries[index as unknown as number],
);
export const latestChangelog = changelogEntryArray.length;

View File

@@ -1848,109 +1848,109 @@ export const emojiDictionary = {
england: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", england: "🏴󠁧󠁢󠁥󠁮󠁧󠁿",
scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿",
wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿",
// ...{ ...{
// 1984: "custom:1984.gif", 1984: "custom:1984.gif",
// KekW: "custom:KekW.png", KekW: "custom:KekW.png",
// amogus: "custom:amogus.gif", amogus: "custom:amogus.gif",
// awaa: "custom:awaa.png", awaa: "custom:awaa.png",
// boohoo: "custom:boohoo.png", boohoo: "custom:boohoo.png",
// boohoo_goes_hard: "custom:boohoo_goes_hard.png", boohoo_goes_hard: "custom:boohoo_goes_hard.png",
// boohoo_shaken: "custom:boohoo_shaken.png", boohoo_shaken: "custom:boohoo_shaken.png",
// cat_arrival: "custom:cat_arrival.gif", cat_arrival: "custom:cat_arrival.gif",
// cat_awson: "custom:cat_awson.png", cat_awson: "custom:cat_awson.png",
// cat_blob: "custom:cat_blob.png", cat_blob: "custom:cat_blob.png",
// cat_bonk: "custom:cat_bonk.png", cat_bonk: "custom:cat_bonk.png",
// cat_concern: "custom:cat_concern.png", cat_concern: "custom:cat_concern.png",
// cat_fast: "custom:cat_fast.gif", cat_fast: "custom:cat_fast.gif",
// cat_kitty: "custom:cat_kitty.png", cat_kitty: "custom:cat_kitty.png",
// cat_lick: "custom:cat_lick.gif", cat_lick: "custom:cat_lick.gif",
// cat_not_like: "custom:cat_not_like.png", cat_not_like: "custom:cat_not_like.png",
// cat_put: "custom:cat_put.gif", cat_put: "custom:cat_put.gif",
// cat_pwease: "custom:cat_pwease.png", cat_pwease: "custom:cat_pwease.png",
// cat_rage: "custom:cat_rage.png", cat_rage: "custom:cat_rage.png",
// cat_sad: "custom:cat_sad.png", cat_sad: "custom:cat_sad.png",
// cat_snuff: "custom:cat_snuff.gif", cat_snuff: "custom:cat_snuff.gif",
// cat_spin: "custom:cat_spin.gif", cat_spin: "custom:cat_spin.gif",
// cat_squish: "custom:cat_squish.gif", cat_squish: "custom:cat_squish.gif",
// cat_stare: "custom:cat_stare.gif", cat_stare: "custom:cat_stare.gif",
// cat_steal: "custom:cat_steal.gif", cat_steal: "custom:cat_steal.gif",
// cat_sussy: "custom:cat_sussy.gif", cat_sussy: "custom:cat_sussy.gif",
// clueless: "custom:clueless.png", clueless: "custom:clueless.png",
// death: "custom:death.gif", death: "custom:death.gif",
// developers: "custom:developers.gif", developers: "custom:developers.gif",
// fastwawa: "custom:fastwawa.gif", fastwawa: "custom:fastwawa.gif",
// ferris: "custom:ferris.png", ferris: "custom:ferris.png",
// ferris_bongo: "custom:ferris_bongo.gif", ferris_bongo: "custom:ferris_bongo.gif",
// ferris_nom: "custom:ferris_nom.png", ferris_nom: "custom:ferris_nom.png",
// ferris_pensive: "custom:ferris_pensive.png", ferris_pensive: "custom:ferris_pensive.png",
// ferris_unsafe: "custom:ferris_unsafe.png", ferris_unsafe: "custom:ferris_unsafe.png",
// flesh: "custom:flesh.png", flesh: "custom:flesh.png",
// flooshed: "custom:flooshed.png", flooshed: "custom:flooshed.png",
// flosh: "custom:flosh.png", flosh: "custom:flosh.png",
// flushee: "custom:flushee.png", flushee: "custom:flushee.png",
// forgor: "custom:forgor.png", forgor: "custom:forgor.png",
// hollow: "custom:hollow.png", hollow: "custom:hollow.png",
// john: "custom:john.png", john: "custom:john.png",
// lightspeed: "custom:lightspeed.png", lightspeed: "custom:lightspeed.png",
// little_guy: "custom:little_guy.png", little_guy: "custom:little_guy.png",
// lmaoooo: "custom:lmaoooo.gif", lmaoooo: "custom:lmaoooo.gif",
// lol: "custom:lol.png", lol: "custom:lol.png",
// looking: "custom:looking.gif", looking: "custom:looking.gif",
// marie: "custom:marie.png", marie: "custom:marie.png",
// marie_furret: "custom:marie_furret.gif", marie_furret: "custom:marie_furret.gif",
// marie_smug: "custom:marie_smug.png", marie_smug: "custom:marie_smug.png",
// megumin: "custom:megumin.png", megumin: "custom:megumin.png",
// michi_above: "custom:michi_above.png", michi_above: "custom:michi_above.png",
// michi_awww: "custom:michi_awww.gif", michi_awww: "custom:michi_awww.gif",
// michi_drag: "custom:michi_drag.gif", michi_drag: "custom:michi_drag.gif",
// michi_flustered: "custom:michi_flustered.png", michi_flustered: "custom:michi_flustered.png",
// michi_glare: "custom:michi_glare.png", michi_glare: "custom:michi_glare.png",
// michi_sus: "custom:michi_sus.png", michi_sus: "custom:michi_sus.png",
// monkaS: "custom:monkaS.png", monkaS: "custom:monkaS.png",
// monkaStare: "custom:monkaStare.png", monkaStare: "custom:monkaStare.png",
// monkey_grr: "custom:monkey_grr.png", monkey_grr: "custom:monkey_grr.png",
// monkey_pensive: "custom:monkey_pensive.png", monkey_pensive: "custom:monkey_pensive.png",
// monkey_zany: "custom:monkey_zany.png", monkey_zany: "custom:monkey_zany.png",
// nazu_sit: "custom:nazu_sit.png", nazu_sit: "custom:nazu_sit.png",
// nazu_sus: "custom:nazu_sus.png", nazu_sus: "custom:nazu_sus.png",
// ok_and: "custom:ok_and.gif", ok_and: "custom:ok_and.gif",
// owo: "custom:owo.png", owo: "custom:owo.png",
// pat: "custom:pat.png", pat: "custom:pat.png",
// pointThink: "custom:pointThink.png", pointThink: "custom:pointThink.png",
// rainbowHype: "custom:rainbowHype.gif", rainbowHype: "custom:rainbowHype.gif",
// rawr: "custom:rawr.png", rawr: "custom:rawr.png",
// rember: "custom:rember.png", rember: "custom:rember.png",
// revolt: "custom:revolt.png", revolt: "custom:revolt.png",
// sickly: "custom:sickly.png", sickly: "custom:sickly.png",
// stare: "custom:stare.png", stare: "custom:stare.png",
// tfyoulookingat: "custom:tfyoulookingat.png", tfyoulookingat: "custom:tfyoulookingat.png",
// thanks: "custom:thanks.png", thanks: "custom:thanks.png",
// thonk: "custom:thonk.png", thonk: "custom:thonk.png",
// trol: "custom:trol.png", trol: "custom:trol.png",
// troll_smile: "custom:troll_smile.gif", troll_smile: "custom:troll_smile.gif",
// uber: "custom:uber.png", uber: "custom:uber.png",
// ubertroll: "custom:ubertroll.png", ubertroll: "custom:ubertroll.png",
// verycool: "custom:verycool.png", verycool: "custom:verycool.png",
// verygood: "custom:verygood.png", verygood: "custom:verygood.png",
// wawafast: "custom:wawafast.gif", wawafast: "custom:wawafast.gif",
// wawastance: "custom:wawastance.png", wawastance: "custom:wawastance.png",
// yeahokayyy: "custom:yeahokayyy.png", yeahokayyy: "custom:yeahokayyy.png",
// yed: "custom:yed.png", yed: "custom:yed.png",
// yems: "custom:yems.png", yems: "custom:yems.png",
// michael: "custom:michael.gif", michael: "custom:michael.gif",
// charle: "custom:charle.gif", charle: "custom:charle.gif",
// sadge: "custom:sadge.webp", sadge: "custom:sadge.webp",
// sus: "custom:sus.webp", sus: "custom:sus.webp",
// chade: "custom:chade.gif", chade: "custom:chade.gif",
// gigachad: "custom:gigachad.webp", gigachad: "custom:gigachad.webp",
// sippy: "custom:sippy.webp", sippy: "custom:sippy.webp",
// ayame_heart: "custom:ayame_heart.png", ayame_heart: "custom:ayame_heart.png",
// catgirl_peek: "custom:catgirl_peek.png", catgirl_peek: "custom:catgirl_peek.png",
// girl_happy: "custom:girl_happy.png", girl_happy: "custom:girl_happy.png",
// hug_plushie: "custom:hug_plushie.png", hug_plushie: "custom:hug_plushie.png",
// huggies: "custom:huggies.png", huggies: "custom:huggies.png",
// noted: "custom:noted.gif", noted: "custom:noted.gif",
// waving: "custom:waving.png", waving: "custom:waving.png",
// mogusvented: "custom:mogusvented.png", mogusvented: "custom:mogusvented.png",
// }, },
}; };

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

@@ -64,7 +64,7 @@ export function useAutoComplete(
const cursor = el.selectionStart; const cursor = el.selectionStart;
const content = el.value.slice(0, cursor); const content = el.value.slice(0, cursor);
const valid = /[\w\-]/; const valid = /\w/;
let j = content.length - 1; let j = content.length - 1;
if (content[j] === "@") { if (content[j] === "@") {

View File

@@ -43,11 +43,11 @@ function toCodePoint(rune: string) {
} }
export function parseEmoji(emoji: string) { export function parseEmoji(emoji: string) {
// if (emoji.startsWith("custom:")) { if (emoji.startsWith("custom:")) {
// return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring( return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
// 7, 7,
// )}`; )}`;
// } }
const codepoint = toCodePoint(emoji); const codepoint = toCodePoint(emoji);
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`; return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;

View File

@@ -5,7 +5,7 @@ import { useTriggerEvents } from "preact-context-menu";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Category } from "@revoltchat/ui"; import { Category, Button } from "@revoltchat/ui";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
@@ -33,7 +33,7 @@ import InviteList from "./embed/EmbedInvite";
interface Props { interface Props {
attachContext?: boolean; attachContext?: boolean;
queued?: QueuedMessage; queued?: QueuedMessage;
message: MessageObject & { webhook: { name: string; avatar?: string } }; message: MessageObject;
highlight?: boolean; highlight?: boolean;
contrast?: boolean; contrast?: boolean;
content?: Children; content?: Children;
@@ -63,7 +63,6 @@ const Message = observer(
? useTriggerEvents("Menu", { ? useTriggerEvents("Menu", {
user: message.author_id, user: message.author_id,
contextualChannel: message.channel_id, contextualChannel: message.channel_id,
contextualMessage: message._id,
// eslint-disable-next-line // eslint-disable-next-line
}) })
: undefined; : undefined;
@@ -138,11 +137,6 @@ const Message = observer(
<UserIcon <UserIcon
className="avatar" className="avatar"
url={message.generateMasqAvatarURL()} url={message.generateMasqAvatarURL()}
override={
message.webhook?.avatar
? `https://autumn.revolt.chat/avatars/${message.webhook.avatar}`
: undefined
}
target={user} target={user}
size={36} size={36}
onClick={handleUserClick} onClick={handleUserClick}
@@ -163,7 +157,6 @@ const Message = observer(
showServerIdentity showServerIdentity
onClick={handleUserClick} onClick={handleUserClick}
masquerade={message.masquerade!} masquerade={message.masquerade!}
override={message.webhook?.name}
{...userContext} {...userContext}
/> />
<MessageDetail <MessageDetail

View File

@@ -23,7 +23,6 @@ import {
} from "../../../lib/renderer/Singleton"; } from "../../../lib/renderer/Singleton";
import { state, useApplicationState } from "../../../mobx/State"; import { state, useApplicationState } from "../../../mobx/State";
import { DraftObject } from "../../../mobx/stores/Draft";
import { Reply } from "../../../mobx/stores/MessageQueue"; import { Reply } from "../../../mobx/stores/MessageQueue";
import { dayjs } from "../../../context/Locale"; import { dayjs } from "../../../context/Locale";
@@ -278,12 +277,7 @@ export default observer(({ channel }: Props) => {
// Push message content to draft. // Push message content to draft.
const setMessage = useCallback( const setMessage = useCallback(
(content?: string) => { (content?: string) => state.draft.set(channel._id, content),
const dobj: DraftObject = {
content,
};
state.draft.set(channel._id, dobj);
},
[state.draft, channel._id], [state.draft, channel._id],
); );
@@ -305,7 +299,7 @@ export default observer(({ channel }: Props) => {
if (!state.draft.has(channel._id)) { if (!state.draft.has(channel._id)) {
setMessage(text); setMessage(text);
} else { } else {
setMessage(`${state.draft.get(channel._id)?.content}\n${text}`); setMessage(`${state.draft.get(channel._id)}\n${text}`);
} }
} }
@@ -323,8 +317,8 @@ export default observer(({ channel }: Props) => {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
return; return;
const content = state.draft.get(channel._id)?.content?.trim() ?? ""; const content = state.draft.get(channel._id)?.trim() ?? "";
if (uploadState.type !== "none") return sendFile(content); if (uploadState.type === "attached") return sendFile(content);
if (content.length === 0) return; if (content.length === 0) return;
internalEmit("NewMessages", "hide"); internalEmit("NewMessages", "hide");
@@ -406,9 +400,7 @@ export default observer(({ channel }: Props) => {
* @returns * @returns
*/ */
async function sendFile(content: string) { async function sendFile(content: string) {
if (uploadState.type !== "attached" && uploadState.type !== "failed") if (uploadState.type !== "attached") return;
return;
const attachments: string[] = []; const attachments: string[] = [];
setMessage; setMessage;
@@ -534,7 +526,7 @@ export default observer(({ channel }: Props) => {
} }
function isInCodeBlock(cursor: number): boolean { function isInCodeBlock(cursor: number): boolean {
const content = state.draft.get(channel._id)?.content || ""; const content = state.draft.get(channel._id) || "";
const contentBeforeCursor = content.substring(0, cursor); const contentBeforeCursor = content.substring(0, cursor);
let delimiterCount = 0; let delimiterCount = 0;
@@ -615,12 +607,10 @@ export default observer(({ channel }: Props) => {
<HackAlertThisFileWillBeReplaced <HackAlertThisFileWillBeReplaced
onSelect={(emoji) => { onSelect={(emoji) => {
const v = state.draft.get(channel._id); const v = state.draft.get(channel._id);
const cnt: DraftObject = { state.draft.set(
content: channel._id,
(v?.content ? `${v.content} ` : "") + `${v ? `${v} ` : ""}:${emoji}:`,
`:${emoji}:`, );
};
state.draft.set(channel._id, cnt);
}} }}
onClose={closePicker} onClose={closePicker}
/> />
@@ -674,7 +664,7 @@ export default observer(({ channel }: Props) => {
id="message" id="message"
maxLength={2000} maxLength={2000}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
value={state.draft.get(channel._id)?.content ?? ""} value={state.draft.get(channel._id) ?? ""}
padding="var(--message-box-padding)" padding="var(--message-box-padding)"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Enter") { if (e.ctrlKey && e.key === "Enter") {
@@ -746,11 +736,13 @@ export default observer(({ channel }: Props) => {
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
/> />
<Action> {state.experiments.isEnabled("picker") && (
<IconButton onClick={() => setPicker(!picker)}> <Action>
<HappyBeaming size={24} /> <IconButton onClick={() => setPicker(!picker)}>
</IconButton> <HappyBeaming size={24} />
</Action> </IconButton>
</Action>
)}
<Action> <Action>
<IconButton <IconButton
className={ className={

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?.replace("attachments", "attachments/download");
const filesize = determineFileSize(size); const filesize = determineFileSize(size);
@@ -49,11 +49,10 @@ export default function AttachmentActions({ attachment }: Props) {
</IconButton> </IconButton>
</a> </a>
<a <a
target="_blank"
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
// target={isFirefox || window.native ? "_blank" : "_self"} target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />

View File

@@ -119,18 +119,19 @@ export const MessageOverlayBar = observer(
</Tooltip> </Tooltip>
)} )}
{message.channel?.havePermission("React") && ( {message.channel?.havePermission("React") &&
<ReactionWrapper state.experiments.isEnabled("picker") && (
open={reactionsOpen} <ReactionWrapper
setOpen={setReactionsOpen} open={reactionsOpen}
message={message}> setOpen={setReactionsOpen}
<Tooltip content="React"> message={message}>
<Entry> <Tooltip content="React">
<HappyBeaming size={18} /> <Entry>
</Entry> <HappyBeaming size={18} />
</Tooltip> </Entry>
</ReactionWrapper> </Tooltip>
)} </ReactionWrapper>
)}
{isAuthor && ( {isAuthor && (
<Tooltip content="Edit"> <Tooltip content="Edit">

View File

@@ -17,7 +17,6 @@ import { Bar } from "./JumpToBottom";
export default observer( export default observer(
({ channel, last_id }: { channel: Channel; last_id?: string }) => { ({ channel, last_id }: { channel: Channel; last_id?: string }) => {
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
const [timeAgo, setTimeAgo] = useState("");
const hide = () => setHidden(true); const hide = () => setHidden(true);
useEffect(() => setHidden(false), [last_id]); useEffect(() => setHidden(false), [last_id]);
@@ -30,14 +29,6 @@ export default observer(
return () => document.removeEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown);
}, []); }, []);
useEffect(() => {
if (last_id) {
try {
setTimeAgo(dayjs(decodeTime(last_id)).fromNow());
} catch (err) {}
}
}, [last_id]);
const renderer = getRenderer(channel); const renderer = getRenderer(channel);
const history = useHistory(); const history = useHistory();
if (renderer.state !== "RENDER") return null; if (renderer.state !== "RENDER") return null;
@@ -61,7 +52,7 @@ export default observer(
<Text <Text
id="app.main.channel.misc.new_messages" id="app.main.channel.misc.new_messages"
fields={{ fields={{
time_ago: timeAgo, time_ago: dayjs(decodeTime(last_id)).fromNow(),
}} }}
/> />
</div> </div>

View File

@@ -76,9 +76,7 @@ export default observer(({ channel }: Props) => {
if (users.length >= 5) { if (users.length >= 5) {
text = <Text id="app.main.channel.typing.several" />; text = <Text id="app.main.channel.typing.several" />;
} else if (users.length > 1) { } else if (users.length > 1) {
const userlist = [...users].map( const userlist = [...users].map((x) => x!.username);
(x) => x!.display_name ?? x!.username,
);
const user = userlist.pop(); const user = userlist.pop();
text = ( text = (
@@ -94,9 +92,7 @@ export default observer(({ channel }: Props) => {
text = ( text = (
<Text <Text
id="app.main.channel.typing.single" id="app.main.channel.typing.single"
fields={{ fields={{ user: users[0]!.username }}
user: users[0]!.display_name ?? users[0]!.username,
}}
/> />
); );
} }

View File

@@ -67,8 +67,7 @@ export default function Embed({ embed }: Props) {
break; break;
} }
case "Twitch": case "Twitch":
case "Lightspeed": case "Lightspeed": {
case "Streamable": {
mw = 1280; mw = 1280;
mh = 720; mh = 720;
break; break;
@@ -142,11 +141,7 @@ export default function Embed({ embed }: Props) {
<a <a
onMouseDown={(ev) => onMouseDown={(ev) =>
(ev.button === 0 || ev.button === 1) && (ev.button === 0 || ev.button === 1) &&
modalController.openLink( modalController.openLink(embed.url!)
embed.url!,
undefined,
true,
)
} }
className={styles.title}> className={styles.title}>
{embed.title} {embed.title}
@@ -198,8 +193,7 @@ export default function Embed({ embed }: Props) {
modalController.push({ type: "image_viewer", embed }) modalController.push({ type: "image_viewer", embed })
} }
onMouseDown={(ev) => onMouseDown={(ev) =>
ev.button === 1 && ev.button === 1 && modalController.openLink(embed.url)
modalController.openLink(embed.url, undefined, true)
} }
/> />
); );

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

@@ -92,16 +92,6 @@ export default function EmbedMedia({ embed, width, height }: Props) {
/> />
); );
} }
case "Streamable": {
return (
<iframe
src={`https://streamable.com/e/${embed.special.id}?loop=0`}
seamless
loading="lazy"
style={{ height }}
/>
);
}
default: { default: {
if (embed.video) { if (embed.video) {
const url = embed.video.url; const url = embed.video.url;

View File

@@ -22,11 +22,12 @@ enum Badges {
const BadgesBase = styled.div` const BadgesBase = styled.div`
gap: 8px; gap: 8px;
display: flex; display: flex;
margin-top: 4px;
flex-direction: row; flex-direction: row;
img { img {
width: 24px; width: 32px;
height: 24px; height: 32px;
} }
`; `;
@@ -102,7 +103,7 @@ export default function UserBadges({ badges, uid }: Props) {
content={ content={
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" /> <Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
}> }>
<Shield size={24} color="gray" /> <Shield size={32} color="gray" />
</Tooltip> </Tooltip>
) : ( ) : (
<></> <></>
@@ -119,7 +120,7 @@ export default function UserBadges({ badges, uid }: Props) {
}} }}
onClick={() => { onClick={() => {
window.open( window.open(
"https://wiki.revolt.chat/notes/project/financial-support/", "https://insrt.uk/donate",
"_blank", "_blank",
); );
}} }}

View File

@@ -29,14 +29,9 @@ const HeaderBase = styled.div`
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.new-name {
font-size: 16px;
font-weight: 600;
}
.username { .username {
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
@@ -55,9 +50,6 @@ export default observer(({ user }: Props) => {
return ( return (
<Header topBorder palette="secondary"> <Header topBorder palette="secondary">
<HeaderBase> <HeaderBase>
<div className="new-name">
{user.display_name ?? user.username}
</div>
<Localizer> <Localizer>
<Tooltip content={<Text id="app.special.copy_username" />}> <Tooltip content={<Text id="app.special.copy_username" />}>
<span <span
@@ -65,9 +57,7 @@ export default observer(({ user }: Props) => {
onClick={() => onClick={() =>
modalController.writeText(user.username) modalController.writeText(user.username)
}> }>
{user.username} @{user.username}
{"#"}
{user.discriminator}
</span> </span>
</Tooltip> </Tooltip>
</Localizer> </Localizer>

View File

@@ -14,7 +14,6 @@ import IconBase, { IconBaseProps } from "../IconBase";
type VoiceStatus = "muted" | "deaf"; type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps<User> { interface Props extends IconBaseProps<User> {
status?: boolean; status?: boolean;
override?: string;
voice?: VoiceStatus; voice?: VoiceStatus;
masquerade?: API.Masquerade; masquerade?: API.Masquerade;
showServerIdentity?: boolean; showServerIdentity?: boolean;
@@ -71,15 +70,12 @@ export default observer(
showServerIdentity, showServerIdentity,
masquerade, masquerade,
innerRef, innerRef,
override,
...svgProps ...svgProps
} = props; } = props;
let { url } = props; let { url } = props;
if (masquerade?.avatar) { if (masquerade?.avatar) {
url = client.proxyFile(masquerade.avatar); url = client.proxyFile(masquerade.avatar);
} else if (override) {
url = override;
} else if (!url) { } else if (!url) {
let override; let override;
if (target && showServerIdentity) { if (target && showServerIdentity) {

View File

@@ -39,7 +39,6 @@ type UsernameProps = Omit<
masquerade?: API.Masquerade; masquerade?: API.Masquerade;
showServerIdentity?: boolean | "both"; showServerIdentity?: boolean | "both";
override?: string;
innerRef?: Ref<any>; innerRef?: Ref<any>;
}; };
@@ -65,18 +64,13 @@ export const Username = observer(
masquerade, masquerade,
showServerIdentity, showServerIdentity,
innerRef, innerRef,
override,
...otherProps ...otherProps
}: UsernameProps) => { }: UsernameProps) => {
let username = let username = user?.username;
(user as unknown as { display_name: string })?.display_name ??
user?.username;
let color = masquerade?.colour; let color = masquerade?.colour;
let timed_out: Date | undefined; let timed_out: Date | undefined;
if (override) { if (user && showServerIdentity) {
username = override;
} else if (user && showServerIdentity) {
const { server } = useParams<{ server?: string }>(); const { server } = useParams<{ server?: string }>();
if (server) { if (server) {
const client = useClient(); const client = useClient();
@@ -152,17 +146,6 @@ export const Username = observer(
); );
} }
if (override) {
return (
<>
{el}
<BotBadge>
<Text id="app.main.channel.bot" />
</BotBadge>
</>
);
}
return el; return el;
}, },
); );

View File

@@ -1,4 +1,5 @@
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import rehypeKatex from "rehype-katex";
import rehypePrism from "rehype-prism"; import rehypePrism from "rehype-prism";
import rehypeReact from "rehype-react"; import rehypeReact from "rehype-react";
import remarkBreaks from "remark-breaks"; import remarkBreaks from "remark-breaks";
@@ -13,9 +14,6 @@ import { createElement } from "preact";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useLayoutEffect, useMemo, useState } from "preact/hooks"; import { useLayoutEffect, useMemo, useState } from "preact/hooks";
// @ts-expect-error no typings
import rehypeKatex from "@revoltchat/rehype-katex";
import { MarkdownProps } from "./Markdown"; import { MarkdownProps } from "./Markdown";
import { handlers } from "./hast"; import { handlers } from "./hast";
import { RenderCodeblock } from "./plugins/Codeblock"; import { RenderCodeblock } from "./plugins/Codeblock";
@@ -148,7 +146,6 @@ const render = unified()
.use(rehypeKatex, { .use(rehypeKatex, {
maxSize: 10, maxSize: 10,
maxExpand: 0, maxExpand: 0,
maxLength: 512,
trust: false, trust: false,
strict: false, strict: false,
output: "html", output: "html",
@@ -185,8 +182,7 @@ const Container = styled.div<{ largeEmoji: boolean }>`
/** /**
* Regex for matching execessive recursion of blockquotes and lists * Regex for matching execessive recursion of blockquotes and lists
*/ */
const RE_RECURSIVE = const RE_RECURSIVE = /(^(?:[>*+-][^\S\r\n]*){5})(?:[>*+-][^\S\r\n]*)+(.*$)/gm;
/(^(?:(?:[>*+-]|\d+\.)[^\S\r\n]*){5})(?:(?:[>*+-]|\d+\.)[^\S\r\n]*)+(.*$)/gm;
/** /**
* Regex for matching multi-line blockquotes * Regex for matching multi-line blockquotes
@@ -248,13 +244,9 @@ export default memo(({ content, disallowBigEmoji }: MarkdownProps) => {
const [Content, setContent] = useState<React.ReactElement>(null!); const [Content, setContent] = useState<React.ReactElement>(null!);
useLayoutEffect(() => { useLayoutEffect(() => {
try { render
render .process(sanitisedContent)
.process(sanitisedContent) .then((file) => setContent(file.result));
.then((file) => setContent(file.result));
} catch (err) {
setContent("Message failed to render." as never);
}
}, [sanitisedContent]); }, [sanitisedContent]);
const largeEmoji = useMemo( const largeEmoji = useMemo(

View File

@@ -27,11 +27,7 @@ export function RenderAnchor({
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={(ev) => onClick={(ev) =>
modalController.openLink( modalController.openLink(href) && ev.preventDefault()
href,
undefined,
ev.currentTarget.innerText !== href,
) && ev.preventDefault()
} }
/> />
); );

View File

@@ -25,7 +25,7 @@ const Emoji = styled.img`
} }
`; `;
const RE_EMOJI = /:([a-zA-Z0-9\-_]+):/g; const RE_EMOJI = /:([a-zA-Z0-9_+]+):/g;
const RE_ULID = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; const RE_ULID = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
export function RenderEmoji({ match }: CustomComponentProps) { export function RenderEmoji({ match }: CustomComponentProps) {
@@ -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

@@ -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

@@ -39,7 +39,7 @@ export default function AccountManagement() {
<CategoryButton <CategoryButton
icon={<Block size={24} color="var(--error)" />} icon={<Block size={24} color="var(--error)" />}
description={ description={
<Text id="app.settings.pages.account.manage.disable_description" /> "Disable your account. You won't be able to access it unless you contact support."
} }
action="chevron" action="chevron"
onClick={callback("disable")}> onClick={callback("disable")}>
@@ -49,7 +49,7 @@ export default function AccountManagement() {
<CategoryButton <CategoryButton
icon={<Trash size={24} color="var(--error)" />} icon={<Trash size={24} color="var(--error)" />}
description={ description={
<Text id="app.settings.pages.account.manage.delete_description" /> "Your account will be queued for deletion, a confirmation email will be sent."
} }
action="chevron" action="chevron"
onClick={callback("delete")}> onClick={callback("delete")}>

View File

@@ -37,13 +37,7 @@ export default observer(() => {
{( {(
[ [
[ ["username", client.user!.username, At],
"username",
client.user!.username +
"#" +
client.user!.discriminator,
At,
],
["email", email, Envelope], ["email", email, Envelope],
["password", "•••••••••", Key], ["password", "•••••••••", Key],
] as const ] as const

View File

@@ -159,7 +159,7 @@ class ClientController {
}) })
.catch((err) => { .catch((err) => {
const error = takeError(err); const error = takeError(err);
if (error === "Unauthorized") { if (error === "Forbidden" || error === "Unauthorized") {
this.sessions.delete(user_id); this.sessions.delete(user_id);
this.current = null; this.current = null;
this.pickNextSession(); this.pickNextSession();
@@ -252,7 +252,7 @@ class ClientController {
// Start client lifecycle // Start client lifecycle
this.addSession( this.addSession(
{ {
session: session as never, session,
}, },
"new", "new",
); );

View File

@@ -3,7 +3,6 @@ import { API, Client } from "revolt.js";
import { state } from "../../mobx/State"; import { state } from "../../mobx/State";
import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar";
import { modalController } from "../modals/ModalController"; import { modalController } from "../modals/ModalController";
/** /**
@@ -98,7 +97,6 @@ export default class Session {
* Called when the client signals it has received the Ready packet * Called when the client signals it has received the Ready packet
*/ */
private onReady() { private onReady() {
resetMemberSidebarFetched();
this.emit({ this.emit({
action: "SUCCESS", action: "SUCCESS",
}); });

View File

@@ -10,9 +10,8 @@ export function takeError(error: any): string {
case 429: case 429:
return "TooManyRequests"; return "TooManyRequests";
case 401: case 401:
return "Unauthorized"
case 403: case 403:
return "Forbidden"; return "Unauthorized";
default: default:
return "UnknownError"; return "UnknownError";
} }

View File

@@ -12,7 +12,7 @@ import { IconButton, Preloader } from "@revoltchat/ui";
import { determineFileSize } from "../../../../lib/fileSize"; import { determineFileSize } from "../../../../lib/fileSize";
import { modalController } from "../../../modals/ModalController"; import { modalController } from "../../../modals/ModalController";
import { clientController, useClient } from "../../ClientController"; import { useClient } from "../../ClientController";
import { takeError } from "../error"; import { takeError } from "../error";
type BehaviourType = type BehaviourType =
@@ -67,16 +67,9 @@ export async function uploadFile(
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const client = clientController.getActiveSession()?.client;
const sesToken =
typeof client?.session === "string"
? client.session
: client?.session?.token;
const res = await Axios.post(`${autumnURL}/${tag}`, formData, { const res = await Axios.post(`${autumnURL}/${tag}`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
"X-Session-Token": sesToken,
}, },
...config, ...config,
}); });

View File

@@ -18,7 +18,6 @@ import { history } from "../../context/history";
import AddFriend from "./components/AddFriend"; import AddFriend from "./components/AddFriend";
import BanMember from "./components/BanMember"; import BanMember from "./components/BanMember";
import Changelog from "./components/Changelog"; import Changelog from "./components/Changelog";
import ChangelogUsernames from "./components/ChangelogUsernames";
import ChannelInfo from "./components/ChannelInfo"; import ChannelInfo from "./components/ChannelInfo";
import Clipboard from "./components/Clipboard"; import Clipboard from "./components/Clipboard";
import ConfirmLeave from "./components/ConfirmLeave"; import ConfirmLeave from "./components/ConfirmLeave";
@@ -40,11 +39,8 @@ import MFAEnableTOTP from "./components/MFAEnableTOTP";
import MFAFlow from "./components/MFAFlow"; import MFAFlow from "./components/MFAFlow";
import MFARecovery from "./components/MFARecovery"; import MFARecovery from "./components/MFARecovery";
import ModifyAccount from "./components/ModifyAccount"; import ModifyAccount from "./components/ModifyAccount";
import ModifyDisplayname from "./components/ModifyDisplayname";
import OutOfDate from "./components/OutOfDate"; import OutOfDate from "./components/OutOfDate";
import PendingFriendRequests from "./components/PendingFriendRequests"; import PendingFriendRequests from "./components/PendingFriendRequests";
import ReportContent from "./components/Report";
import ReportSuccess from "./components/ReportSuccess";
import ServerIdentity from "./components/ServerIdentity"; import ServerIdentity from "./components/ServerIdentity";
import ServerInfo from "./components/ServerInfo"; import ServerInfo from "./components/ServerInfo";
import ShowToken from "./components/ShowToken"; import ShowToken from "./components/ShowToken";
@@ -208,25 +204,12 @@ class ModalControllerExtended extends ModalController<Modal> {
* Safely open external or internal link * Safely open external or internal link
* @param href Raw URL * @param href Raw URL
* @param trusted Whether we trust this link * @param trusted Whether we trust this link
* @param mismatch Whether to always open link warning
* @returns Whether to cancel default event * @returns Whether to cancel default event
*/ */
openLink(href?: string, trusted?: boolean, mismatch?: boolean) { openLink(href?: string, trusted?: boolean) {
const link = determineLink(href); const link = determineLink(href);
const settings = getApplicationState().settings; const settings = getApplicationState().settings;
if (mismatch) {
if (href) {
modalController.push({
type: "link_warning",
link: href,
callback: () => this.openLink(href, true) as true,
});
}
return true;
}
switch (link.type) { switch (link.type) {
case "navigate": { case "navigate": {
history.push(link.path); history.push(link.path);
@@ -293,9 +276,4 @@ export const modalController = new ModalControllerExtended({
sign_out_sessions: SignOutSessions, sign_out_sessions: SignOutSessions,
user_picker: UserPicker, user_picker: UserPicker,
user_profile: UserProfile, user_profile: UserProfile,
report: ReportContent,
report_success: ReportSuccess,
modify_displayname: ModifyDisplayname,
changelog_usernames: ChangelogUsernames,
reset_bot_token: Confirmation
}); });

View File

@@ -9,17 +9,16 @@ export default observer(() => {
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
function keyDown(event: KeyboardEvent) { function keyUp(event: KeyboardEvent) {
if (event.key === "Escape") { if (event.key === "Escape") {
modalController.pop("close"); modalController.pop("close");
} else if (event.key === "Enter") { } else if (event.key === "Enter") {
if (event.target instanceof HTMLSelectElement) return;
modalController.pop("confirm"); modalController.pop("confirm");
} }
} }
document.addEventListener("keydown", keyDown); document.addEventListener("keyup", keyUp);
return () => document.removeEventListener("keydown", keyDown); return () => document.removeEventListener("keyup", keyUp);
}, []); }, []);
return ( return (

View File

@@ -23,7 +23,6 @@ export default function AddFriend({ ...props }: ModalProps<"add_friend">) {
data={{ data={{
username: { username: {
field: "Username", field: "Username",
placeholder: "username#1234",
}, },
}} }}
callback={({ username }) => callback={({ username }) =>

View File

@@ -1,5 +1,5 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import styled, { css } from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useMemo, useState } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
@@ -14,17 +14,10 @@ import {
changelogEntryArray, changelogEntryArray,
ChangelogPost, ChangelogPost,
} from "../../../assets/changelogs"; } from "../../../assets/changelogs";
import Markdown from "../../../components/markdown/Markdown";
import { ModalProps } from "../types"; import { ModalProps } from "../types";
const Image = styled.img<{ shadow?: boolean }>` const Image = styled.img`
border-radius: var(--border-radius); border-radius: var(--border-radius);
${(props) =>
props.shadow &&
css`
filter: drop-shadow(4px 4px 10px rgba(0, 0, 0, 0.5));
`}
`; `;
function RenderLog({ post }: { post: ChangelogPost }) { function RenderLog({ post }: { post: ChangelogPost }) {
@@ -32,11 +25,9 @@ function RenderLog({ post }: { post: ChangelogPost }) {
<Column> <Column>
{post.content.map((entry) => {post.content.map((entry) =>
typeof entry === "string" ? ( typeof entry === "string" ? (
<Markdown content={entry} /> <span>{entry}</span>
) : entry.type === "element" ? (
entry.element
) : ( ) : (
<Image src={entry.src} shadow={entry.shadow} /> <Image src={entry.src} />
), ),
)} )}
</Column> </Column>

View File

@@ -1,146 +0,0 @@
import Lottie, { LottieRefCurrentProps } from "lottie-react";
import { useEffect, useRef } from "preact/hooks";
import { Button, Column, InputBox, Modal, Row } from "@revoltchat/ui";
import { useClient } from "../../client/ClientController";
import { modalController } from "../ModalController";
import { ModalProps } from "../types";
import usernameAnim from "./legacy/usernameUpdateLottie.json";
/**
* Changelog: Username update
*/
export default function ChangelogUsernames({
onClose,
signal,
}: ModalProps<"changelog_usernames">) {
const client = useClient();
const lottieRef = useRef<LottieRefCurrentProps>();
useEffect(() => {
if (lottieRef.current) {
const timer = setTimeout(() => lottieRef.current!.play(), 2500);
return () => clearTimeout(timer);
}
}, [lottieRef]);
return (
<Modal onClose={onClose} signal={signal} transparent>
{
(
<Column gap="0">
<div
style={{
background: "black",
borderStartStartRadius: "12px",
borderStartEndRadius: "12px",
display: "grid",
placeItems: "center",
padding: "1.5em",
}}>
<Lottie
lottieRef={lottieRef as never}
animationData={usernameAnim}
autoplay={false}
loop={false}
style={{ width: "240px" }}
/>
</div>
<div
style={{
padding: "1em",
background: "var(--secondary-header)",
textAlign: "center",
}}>
<Column
gap="6px"
style={{
alignItems: "center",
}}>
<span
style={{
fontSize: "1.5em",
fontWeight: 700,
marginBottom: "12px",
}}>
Usernames are Changing
</span>
<span
style={{
color: "var(--secondary-foreground)",
fontSize: "0.9em",
fontWeight: 500,
}}>
We've changed how usernames work on Revolt.
Now you can set a display name in addition
to a username with a number tag for easier
sharing.
</span>
<span
style={{
color: "var(--secondary-foreground)",
fontSize: "0.9em",
fontWeight: 500,
}}>
Here's how your new username looks:
</span>
<InputBox
value={
client.user!.display_name ??
client.user!.username
}
style={{
maxWidth: "180px",
}}
disabled
/>
<InputBox
value={
client.user.username +
"#" +
client.user.discriminator
}
style={{
maxWidth: "180px",
}}
disabled
/>
<a
href="https://revolt.chat/posts/evolving-usernames"
target="_blank">
Read more about this change
</a>
</Column>
</div>
<Row
style={{
padding: "1em",
borderEndStartRadius: "12px",
borderEndEndRadius: "12px",
background: "var(--secondary-background)",
textAlign: "center",
justifyContent: "end",
}}>
<Button palette="plain" onClick={onClose}>
Skip for now
</Button>
<Button
palette="accent"
onClick={() => {
modalController.openLink(
"/settings/profile",
);
onClose();
}}>
Edit Profile
</Button>
</Row>
</Column>
) as any
}
</Modal>
);
}

View File

@@ -20,7 +20,6 @@ export default function Confirmation(
| "delete_bot" | "delete_bot"
| "block_user" | "block_user"
| "unfriend_user" | "unfriend_user"
| "reset_bot_token"
>, >,
) { ) {
const history = useHistory(); const history = useHistory();
@@ -32,7 +31,6 @@ export default function Confirmation(
delete_bot: ["confirm_delete", "delete"], delete_bot: ["confirm_delete", "delete"],
unfriend_user: ["unfriend_user", "remove"], unfriend_user: ["unfriend_user", "remove"],
block_user: ["block_user", "block"], block_user: ["block_user", "block"],
reset_bot_token: ["reset_bot_token", "reset"],
}; };
const event = EVENTS[props.type]; const event = EVENTS[props.type];
@@ -90,12 +88,6 @@ export default function Confirmation(
.bots.delete(props.target); .bots.delete(props.target);
props.cb?.(); props.cb?.();
break; break;
case "reset_bot_token":
clientController
.getAvailableClient()
.bots
.edit(props.target.id, { remove: ["Token"] })
.then(props.callback)
} }
}} }}
submit={{ submit={{

View File

@@ -3,7 +3,7 @@ import { SubmitHandler, useForm } from "react-hook-form";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Category, Error, InputBox, Modal, Tip } from "@revoltchat/ui"; import { Category, Error, Modal } from "@revoltchat/ui";
import { noopTrue } from "../../../lib/js"; import { noopTrue } from "../../../lib/js";
@@ -120,34 +120,14 @@ export default function ModifyAccount({
/> />
)} )}
{field === "username" && ( {field === "username" && (
<div <FormField
style={{ type="username"
display: "flex", name="new_username"
alignItems: "end", register={register}
gap: "8px", showOverline
}}> error={errors.new_username?.message}
<div style={{ flexGrow: 1 }}> disabled={processing}
<FormField />
type="username"
name="new_username"
register={register}
showOverline
error={errors.new_username?.message}
disabled={processing}
/>
</div>
<div
style={{
flexShrink: 0,
width: "80px",
textAlign: "center",
}}>
<InputBox
disabled
value={"#" + client.user.discriminator}
/>
</div>
</div>
)} )}
<FormField <FormField
type="current_password" type="current_password"
@@ -161,24 +141,11 @@ export default function ModifyAccount({
<Category compact> <Category compact>
<Error <Error
error={ error={
<> <Text id="app.special.modals.account.failed" />
<Text id="app.special.modals.account.failed" />{" "}
<Text id={`error.${error}`}>{error}</Text>
</>
} }
/> />
</Category> </Category>
)} )}
{field === "username" && (
<div style={{ marginTop: "8px" }}>
<Tip palette="warning">
Changing your username may change your number tag.
You can freely change the case of your username.
Your number tag may change at most once a day.
</Tip>
</div>
)}
</form> </form>
</Modal> </Modal>
); );

View File

@@ -1,47 +0,0 @@
import { Text } from "preact-i18n";
import { ModalForm } from "@revoltchat/ui";
import { useClient } from "../../client/ClientController";
import { ModalProps } from "../types";
/**
* Modify display name modal
*/
export default function ModifyDisplayname({
...props
}: ModalProps<"modify_displayname">) {
const client = useClient();
return (
<ModalForm
{...props}
title="Update display name"
schema={{
display_name: "text",
}}
defaults={{
display_name: (
client.user as unknown as { display_name: string }
)?.display_name as string,
}}
data={{
display_name: {
field: "Display Name",
},
}}
callback={({ display_name }) =>
display_name && display_name !== client.user!.username
? client.users.edit({
display_name,
} as never)
: client.users.edit({
remove: ["DisplayName"],
} as never)
}
submit={{
children: <Text id="app.special.modals.actions.save" />,
}}
/>
);
}

View File

@@ -1,148 +0,0 @@
import { API, Message as MessageInterface, User } from "revolt.js";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { ModalForm, Row } from "@revoltchat/ui";
import Message from "../../../components/common/messaging/Message";
import UserShort from "../../../components/common/user/UserShort";
import { useClient } from "../../client/ClientController";
import { modalController } from "../ModalController";
import { ModalProps } from "../types";
const CONTENT_REASONS: API.ContentReportReason[] = [
"NoneSpecified",
"Illegal",
"PromotesHarm",
"SpamAbuse",
"Malware",
"Harassment",
];
const USER_REASONS: API.UserReportReason[] = [
"NoneSpecified",
"SpamAbuse",
"InappropriateProfile",
"Impersonation",
"BanEvasion",
"Underage",
];
/**
* Add padding to the message container
*/
const MessageContainer = styled.div`
margin-block-end: 16px;
`;
/**
* Report creation modal
*/
export default function ReportContent({
target,
messageId,
...props
}: ModalProps<"report">) {
const client = useClient();
return (
<ModalForm
{...props}
title={
target instanceof MessageInterface ? (
<Text id="app.special.modals.report.message" />
) : (
<Text
id="app.special.modals.report.by_name"
fields={{
name:
target instanceof User
? target.username
: target.name,
}}
/>
)
}
schema={{
selected: "custom",
reason: "combo",
additional_context: "text",
}}
data={{
selected: {
element:
target instanceof MessageInterface ? (
<MessageContainer>
<Message message={target} head attachContext />
</MessageContainer>
) : target instanceof User ? (
<Row centred>
<UserShort user={target} size={32} />
</Row>
) : (
<></>
),
},
reason: {
field: (
<Text id="app.special.modals.report.reason" />
) as React.ReactChild,
options: (target instanceof User
? USER_REASONS
: CONTENT_REASONS
).map((value) => ({
name: (
<Text
id={
value === "NoneSpecified"
? "app.special.modals.report.no_reason"
: `app.special.modals.report.${
target instanceof User
? "user"
: "content"
}_reason.${value}`
}
/>
),
value,
})),
},
additional_context: {
field: (
<Text id="app.special.modals.report.additional_context" />
) as React.ReactChild,
},
}}
callback={async ({ reason, additional_context }) => {
await client.api.post("/safety/report", {
content: {
id: target._id,
type:
target instanceof MessageInterface
? "Message"
: target instanceof User
? "User"
: "Server",
report_reason: reason as any,
message_id: messageId,
},
additional_context,
});
modalController.push({
type: "report_success",
user:
target instanceof MessageInterface
? target.author
: target instanceof User
? target
: undefined,
});
}}
submit={{
children: <Text id="app.special.modals.actions.report" />,
}}
/>
);
}

View File

@@ -1,65 +0,0 @@
import { Text } from "preact-i18n";
import { Modal } from "@revoltchat/ui";
import { noopTrue } from "../../../lib/js";
import { ModalProps } from "../types";
/**
* Report success modal
*/
export default function ReportSuccess({
user,
...props
}: ModalProps<"report_success">) {
return (
<Modal
{...props}
title={<Text id="app.special.modals.report.reported" />}
description={
<>
<Text id="app.special.modals.report.thank_you" />
{user && (
<>
<br />
<br />
<Text id="app.special.modals.report.block_user" />
</>
)}
</>
}
actions={
user
? [
{
palette: "plain",
onClick: async () => {
user.blockUser();
return true;
},
children: (
<Text id="app.special.modals.actions.block" />
),
},
{
palette: "plain-secondary",
onClick: noopTrue,
children: (
<Text id="app.special.modals.actions.dont_block" />
),
},
]
: [
{
palette: "plain",
onClick: noopTrue,
children: (
<Text id="app.special.modals.actions.done" />
),
},
]
}
/>
);
}

View File

@@ -5,7 +5,6 @@ import { Text } from "preact-i18n";
import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui"; import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { useClient } from "../../client/ClientController";
import { report } from "../../safety"; import { report } from "../../safety";
import { modalController } from "../ModalController"; import { modalController } from "../ModalController";
import { ModalProps } from "../types"; import { ModalProps } from "../types";
@@ -14,49 +13,6 @@ export default function ServerInfo({
server, server,
...props ...props
}: ModalProps<"server_info">) { }: ModalProps<"server_info">) {
const client = useClient();
const isOwner = server.owner === client.user?._id;
const actions = [
{
onClick: () => {
modalController.push({
type: "server_identity",
member: server.member!,
});
return true;
},
children: "Edit Identity",
palette: "primary",
},
];
if (!isOwner) {
actions.push({
onClick: () => {
modalController.push({
type: "leave_server",
target: server,
});
return true;
},
children: "Leave Server",
palette: "error",
});
}
actions.push({
onClick: () => {
modalController.push({
type: "report",
target: server,
});
return true;
},
children: <Text id="app.special.modals.actions.report" />,
palette: "error",
});
return ( return (
<Modal <Modal
{...props} {...props}
@@ -70,7 +26,22 @@ export default function ServerInfo({
</IconButton> </IconButton>
</Row> </Row>
} }
actions={actions}> actions={[
{
onClick: () =>
modalController.push({
type: "server_identity",
member: server.member!,
}),
children: "Edit Identity",
palette: "primary",
},
{
onClick: () => report(server),
children: <Text id="app.special.modals.actions.report" />,
palette: "error",
},
]}>
<Markdown content={server.description!} /> <Markdown content={server.description!} />
</Modal> </Modal>
); );

View File

@@ -71,10 +71,6 @@ export function OnboardingModal({
error={error} error={error}
/> />
</div> </div>
<p>
You will be automatically assigned a number
tag which you can find from settings.
</p>
<Button palette="accent"> <Button palette="accent">
{"Looks good!"} {"Looks good!"}
</Button> </Button>

View File

@@ -46,28 +46,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.usernameDetail { .username {
display: flex;
flex-direction: column;
margin-bottom: 4px;
}
.displayname {
font-size: 22px; font-size: 22px;
font-weight: 600; font-weight: 600;
} }
.username {
display: block;
font-size: 13px;
font-weight: 600;
width: fit-content;
color: var(--secondary-foreground);
}
.status { .status {
font-size: 13px; font-size: 13px;
color: var(--secondary-foreground);
> div { > div {
display: inline !important; display: inline !important;

View File

@@ -6,7 +6,6 @@ import {
UserX, UserX,
Group, Group,
InfoCircle, InfoCircle,
Flag,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
@@ -164,33 +163,16 @@ export const UserProfile = observer(
} }
/> />
<div className={styles.details}> <div className={styles.details}>
<div className={styles.usernameDetail}> <Localizer>
<span
className={styles.displayname}
onClick={() =>
modalController.writeText(user.username)
}>
{user.display_name ?? user.username}
</span>
<span <span
className={styles.username} className={styles.username}
onClick={() => onClick={() =>
modalController.writeText( modalController.writeText(user.username)
user.username +
"#" +
user.discriminator,
)
}> }>
<Localizer> {"@"}
<Tooltip {user.username}
content={
<Text id="app.special.copy_username" />
}>
{user.username}#{user.discriminator}
</Tooltip>
</Localizer>
</span> </span>
</div> </Localizer>
{user.status?.text && ( {user.status?.text && (
<span className={styles.status}> <span className={styles.status}>
<UserStatus user={user} tooltip /> <UserStatus user={user} tooltip />
@@ -247,40 +229,7 @@ export const UserProfile = observer(
<UserX size={28} /> <UserX size={28} />
</IconButton> </IconButton>
)} )}
{!user.bot && flags != 2 && flags != 4 && (
<Localizer>
<Tooltip
content={
<Text id="app.context_menu.report_user" />
}>
<IconButton
onClick={() =>
modalController.push({
type: "report",
target: user,
})
}>
<Flag size={28} />
</IconButton>
</Tooltip>
</Localizer>
)}
</div> </div>
{badges > 0 && (
<div
style={{
marginInline: "1em",
padding: "0.5em",
background: "var(--primary-background)",
borderRadius: "8px",
width: "fit-content",
backgroundColor:
"rgba(var(--primary-header-rgb), max(var(--min-opacity), 0.65))",
backdropFilter: "blur(20px)",
}}>
<UserBadges badges={badges} uid={user._id} />
</div>
)}
<div className={styles.tabs}> <div className={styles.tabs}>
<div <div
data-active={tab === "profile"} data-active={tab === "profile"}
@@ -312,7 +261,10 @@ export const UserProfile = observer(
</div> </div>
<div className={styles.content}> <div className={styles.content}>
{tab === "profile" && {tab === "profile" &&
(profile?.content || flags > 0 || user.bot ? ( (profile?.content ||
badges > 0 ||
flags > 0 ||
user.bot ? (
<div> <div>
{flags & 1 ? ( {flags & 1 ? (
/** ! FIXME: i18n this area */ /** ! FIXME: i18n this area */
@@ -362,6 +314,17 @@ export const UserProfile = observer(
</div> </div>
</> </>
) : undefined} ) : undefined}
{badges > 0 && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.badges" />
</div>
)}
{badges > 0 && (
<UserBadges
badges={badges}
uid={user._id}
/>
)}
{profile?.content && ( {profile?.content && (
<> <>
<div className={styles.category}> <div className={styles.category}>

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,6 @@ export type Modal = {
| "create_group" | "create_group"
| "create_server" | "create_server"
| "custom_status" | "custom_status"
| "modify_displayname"
| "add_friend"; | "add_friend";
} }
| ({ | ({
@@ -41,7 +40,6 @@ export type Modal = {
type: "changelog"; type: "changelog";
initial?: number; initial?: number;
} }
| { type: "changelog_usernames" }
| { | {
type: "sign_out_sessions"; type: "sign_out_sessions";
client: Client; client: Client;
@@ -181,20 +179,6 @@ export type Modal = {
| { | {
type: "import_theme"; type: "import_theme";
} }
| {
type: "report";
target: Server | User | Message;
messageId?: string;
}
| {
type: "report_success";
user?: User;
}
| {
type: "reset_bot_token";
target: { name: string, id: string },
callback: () => Promise<void>;
}
); );
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & { export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {

View File

@@ -2,29 +2,32 @@ import { ChevronRight, Trash } from "@styled-icons/boxicons-regular";
import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
import { isFirefox } from "react-device-detect"; import { isFirefox } from "react-device-detect";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel, Message, Server, User, API, Permission, UserPermission, Member } from "revolt.js"; import {
Channel,
Message,
Server,
User,
API,
Permission,
UserPermission,
Member,
} from "revolt.js";
import {
ContextMenuWithData,
import { ContextMenuWithData, MenuItem, openContextMenu } from "preact-context-menu"; MenuItem,
openContextMenu,
} from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { IconButton, LineDivider } from "@revoltchat/ui";
import { Column, IconButton, LineDivider } from "@revoltchat/ui";
import { useApplicationState } from "../mobx/State"; import { useApplicationState } from "../mobx/State";
import { QueuedMessage } from "../mobx/stores/MessageQueue"; import { QueuedMessage } from "../mobx/stores/MessageQueue";
import { NotificationState } from "../mobx/stores/NotificationOptions"; import { NotificationState } from "../mobx/stores/NotificationOptions";
import CMNotifications from "./contextmenu/CMNotifications"; import CMNotifications from "./contextmenu/CMNotifications";
import Tooltip from "../components/common/Tooltip"; import Tooltip from "../components/common/Tooltip";
import UserStatus from "../components/common/user/UserStatus"; import UserStatus from "../components/common/user/UserStatus";
import { useSession } from "../controllers/client/ClientController"; import { useSession } from "../controllers/client/ClientController";
@@ -33,7 +36,6 @@ import { modalController } from "../controllers/modals/ModalController";
import { internalEmit } from "./eventEmitter"; import { internalEmit } from "./eventEmitter";
import { getRenderer } from "./renderer/Singleton"; import { getRenderer } from "./renderer/Singleton";
interface ContextMenuData { interface ContextMenuData {
user?: string; user?: string;
server?: string; server?: string;
@@ -45,13 +47,10 @@ interface ContextMenuData {
unread?: boolean; unread?: boolean;
queued?: QueuedMessage; queued?: QueuedMessage;
contextualChannel?: string; contextualChannel?: string;
contextualMessage?: string;
} }
type Action = type Action =
| { action: "copy_id"; id: string } | { action: "copy_id"; id: string }
| { action: "admin"; id: string; type: "channel" | "message" | "user" }
| { action: "admin_system"; id: string }
| { action: "copy_message_link"; message: Message } | { action: "copy_message_link"; message: Message }
| { action: "copy_selection" } | { action: "copy_selection" }
| { action: "copy_text"; content: string } | { action: "copy_text"; content: string }
@@ -112,8 +111,7 @@ type Action =
action: "set_notification_state"; action: "set_notification_state";
key: string; key: string;
state?: NotificationState; state?: NotificationState;
} };
| { action: "report"; target: User | Server | Message; messageId?: string };
// ! FIXME: I dare someone to re-write this // ! FIXME: I dare someone to re-write this
// Tip: This should just be split into separate context menus per logical area. // Tip: This should just be split into separate context menus per logical area.
@@ -133,18 +131,6 @@ export default function ContextMenus() {
case "copy_id": case "copy_id":
modalController.writeText(data.id); modalController.writeText(data.id);
break; break;
case "admin":
window.open(
`https://admin.revolt.chat/panel/inspect/${data.type}/${data.id}`,
"_blank",
);
break;
case "admin_system":
window.open(
`https://admin.revolt.chat/panel/inspect/user/${data.id}`,
"_blank",
);
break;
case "copy_message_link": case "copy_message_link":
{ {
let pathname = `/channel/${data.message.channel_id}/${data.message._id}`; let pathname = `/channel/${data.message.channel_id}/${data.message._id}`;
@@ -288,7 +274,11 @@ export default function ContextMenus() {
window.open( window.open(
// ! FIXME: do this from revolt.js // ! FIXME: do this from revolt.js
client client
.generateFileURL(data.attachment), .generateFileURL(data.attachment)
?.replace(
"attachments",
"attachments/download",
),
isFirefox || window.native ? "_blank" : "_self", isFirefox || window.native ? "_blank" : "_self",
); );
} }
@@ -459,13 +449,6 @@ export default function ContextMenus() {
case "open_server_settings": case "open_server_settings":
history.push(`/server/${data.id}/settings`); history.push(`/server/${data.id}/settings`);
break; break;
case "report":
modalController.push({
type: "report",
target: data.target,
messageId: data.messageId,
});
break;
} }
})().catch((err) => { })().catch((err) => {
modalController.push({ modalController.push({
@@ -488,7 +471,6 @@ export default function ContextMenus() {
queued, queued,
unread, unread,
contextualChannel: cxid, contextualChannel: cxid,
contextualMessage,
}: ContextMenuData) => { }: ContextMenuData) => {
const elements: Children[] = []; const elements: Children[] = [];
let lastDivider = false; let lastDivider = false;
@@ -504,17 +486,11 @@ export default function ContextMenus() {
elements.push( elements.push(
<MenuItem data={action} disabled={disabled}> <MenuItem data={action} disabled={disabled}>
<span style={{ color }}> <span style={{ color }}>
{locale === "admin" ? ( <Text
"Open in Admin Panel" id={`app.context_menu.${
) : locale === "admin_system" ? ( locale ?? action.action
"Open User in Admin Panel" }`}
) : ( />
<Text
id={`app.context_menu.${
locale ?? action.action
}`}
/>
)}
</span> </span>
{tip && <div className="tip">{tip}</div>} {tip && <div className="tip">{tip}</div>}
</MenuItem>, </MenuItem>,
@@ -693,20 +669,6 @@ export default function ContextMenus() {
} as unknown as Action); } as unknown as Action);
} }
} }
if (user._id !== userId) {
generateAction(
{
action: "report",
target: user,
messageId: contextualMessage,
},
"report_user",
undefined,
undefined,
"var(--error)",
);
}
} }
if (contextualChannel) { if (contextualChannel) {
@@ -833,33 +795,14 @@ export default function ContextMenus() {
}); });
} }
if (message.author_id !== userId) {
generateAction(
{
action: "report",
target: message,
},
"report_message",
undefined,
undefined,
"var(--error)",
);
}
if ( if (
message.author_id === userId || message.author_id === userId ||
channelPermissions & Permission.ManageMessages channelPermissions & Permission.ManageMessages
) { ) {
generateAction( generateAction({
{ action: "delete_message",
action: "delete_message", target: message,
target: message, });
},
undefined,
undefined,
undefined,
"var(--error)",
);
} }
if ( if (
@@ -1092,17 +1035,6 @@ export default function ContextMenus() {
"var(--error)", "var(--error)",
); );
} else { } else {
generateAction(
{
action: "report",
target: server,
},
"report_server",
undefined,
undefined,
"var(--error)",
);
generateAction( generateAction(
{ action: "leave_server", target: server }, { action: "leave_server", target: server },
"leave_server", "leave_server",
@@ -1121,37 +1053,6 @@ export default function ContextMenus() {
} }
if (!hideIDButton) { if (!hideIDButton) {
if (state.experiments.isEnabled("admin_beta")) {
generateAction(
{
action: "admin",
id,
type: cid
? "channel"
: message
? "message"
: "user",
},
"admin",
);
switch (message?.system?.type) {
case "user_added":
case "user_remove":
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
generateAction(
{
action: "admin_system",
id: message.system.id,
},
"admin_system",
);
}
}
generateAction( generateAction(
{ action: "copy_id", id }, { action: "copy_id", id },
cid cid
@@ -1187,21 +1088,7 @@ export default function ContextMenus() {
content={ content={
<Text id="app.special.copy_username" /> <Text id="app.special.copy_username" />
}> }>
<Column gap="0"> @{user.username}
<span>
{user.display_name ??
user.username}
</span>
<span
style={{
fontSize: "0.8em",
}}>
{user.username}
{"#"}
{user.discriminator ??
"0000"}
</span>
</Column>
</Tooltip> </Tooltip>
</div> </div>
<div <div

View File

@@ -17,7 +17,6 @@ const ALLOWED_ORIGINS = [
"app.revolt.chat", "app.revolt.chat",
"nightly.revolt.chat", "nightly.revolt.chat",
"local.revolt.chat", "local.revolt.chat",
"rolt.chat",
]; ];
/** /**

View File

@@ -1,6 +1,6 @@
import { action, makeAutoObservable, runInAction } from "mobx"; import { action, makeAutoObservable, runInAction } from "mobx";
import { changelogEntries, latestChangelog } from "../../assets/changelogs"; import { latestChangelog } from "../../assets/changelogs";
import { modalController } from "../../controllers/modals/ModalController"; import { modalController } from "../../controllers/modals/ModalController";
import Persistent from "../interfaces/Persistent"; import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store"; import Store from "../interfaces/Store";
@@ -58,21 +58,10 @@ export default class Changelog implements Store, Persistent<Data>, Syncable {
*/ */
checkForUpdates() { checkForUpdates() {
if (this.viewed < latestChangelog) { if (this.viewed < latestChangelog) {
const expires = new Date(+changelogEntries[latestChangelog].date); modalController.push({
expires.setDate(expires.getDate() + 7); type: "changelog",
initial: latestChangelog,
if (+new Date() < +expires) { });
if (latestChangelog === 3) {
modalController.push({
type: "changelog_usernames",
});
} else {
modalController.push({
type: "changelog",
initial: latestChangelog,
});
}
}
runInAction(() => { runInAction(() => {
this.viewed = latestChangelog; this.viewed = latestChangelog;

View File

@@ -5,7 +5,7 @@ import { mapToRecord } from "../../lib/conversion";
import Persistent from "../interfaces/Persistent"; import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store"; import Store from "../interfaces/Store";
export interface DraftObject { interface DraftObject {
content?: string; content?: string;
masquerade?: { masquerade?: {
avatar: string; avatar: string;
@@ -59,12 +59,10 @@ export default class Draft implements Store, Persistent<Data> {
* @param channel Channel ID * @param channel Channel ID
*/ */
@computed has(channel: string) { @computed has(channel: string) {
if (!this.drafts.has(channel)) return false; return (
// fetch the draft object this.drafts.has(channel) &&
const potentialDraft = this.drafts.get(channel)?.content; this.drafts.get(channel)!.content!.length > 0
// if it doesn't have any content return false );
if (!potentialDraft) return false;
return potentialDraft.length > 0;
} }
/** /**

View File

@@ -10,7 +10,7 @@ import Store from "../interfaces/Store";
/** /**
* Union type of available experiments. * Union type of available experiments.
*/ */
export type Experiment = "dummy" | "offline_users" | "plugins" | "admin_beta"; export type Experiment = "dummy" | "offline_users" | "plugins" | "picker";
/** /**
* Currently active experiments. * Currently active experiments.
@@ -19,7 +19,7 @@ export const AVAILABLE_EXPERIMENTS: Experiment[] = [
"dummy", "dummy",
"offline_users", "offline_users",
"plugins", "plugins",
"admin_beta", "picker",
]; ];
/** /**
@@ -42,10 +42,10 @@ export const EXPERIMENTS: {
description: description:
"This will enable the experimental plugin API. Only touch this if you know what you're doing.", "This will enable the experimental plugin API. Only touch this if you know what you're doing.",
}, },
admin_beta: { picker: {
title: "Admin Panel Shortcuts", title: "Custom Emoji",
description: description:
"Adds context menu entries to quickly jump to Revolt Admin Panel. This is intended for use by the team.", "This will enable a work-in-progress emoji picker, custom emoji settings and a reaction picker.",
}, },
}; };

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

@@ -28,7 +28,7 @@ const AppContainer = styled.div`
background-position: center center !important; background-position: center center !important;
`; `;
export const StatusBar = styled.div` const StatusBar = styled.div`
height: 40px; height: 40px;
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -1,6 +1,5 @@
import { Hash } from "@styled-icons/boxicons-regular"; import { Hash } from "@styled-icons/boxicons-regular";
import { Ghost } from "@styled-icons/boxicons-solid"; import { Ghost } from "@styled-icons/boxicons-solid";
import dayjs from "dayjs";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router-dom"; import { Redirect, useParams } from "react-router-dom";
@@ -164,7 +163,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const checkUnread = () => const checkUnread = () =>
channel.unread && channel.unread &&
document.hasFocus() &&
channel.client.unreads!.markRead( channel.client.unreads!.markRead(
channel._id, channel._id,
channel.last_message_id!, channel.last_message_id!,
@@ -178,46 +176,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
); );
}, [channel]); }, [channel]);
useEffect(() => {
let lastSubscribed: number | undefined;
function subscribe() {
if (document.hasFocus()) {
if (
!lastSubscribed ||
dayjs().subtract(10, "minutes").isAfter(lastSubscribed)
) {
lastSubscribed = +new Date();
channel.server?.subscribe();
}
}
}
// Trigger logic every minute
const subTimer = setInterval(subscribe, 60e3);
subscribe();
function onFocus() {
// Mark channel as read if it's unread
if (channel.unread) {
channel.client.unreads!.markRead(
channel._id,
channel.last_message_id!,
true,
);
}
// Subscribe to channel if expired
subscribe();
}
addEventListener("focus", onFocus);
return () => {
removeEventListener("focus", onFocus);
clearInterval(subTimer);
};
}, [channel]);
return ( return (
<AgeGate <AgeGate
type="channel" type="channel"

View File

@@ -29,7 +29,6 @@ const VoiceBase = styled.div`
margin-top: 48px; margin-top: 48px;
padding: 20px; padding: 20px;
background: var(--secondary-background); background: var(--secondary-background);
flex-grow: 1;
.status { .status {
flex: 1 0; flex: 1 0;
@@ -55,6 +54,8 @@ const VoiceBase = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: scroll;
.participants { .participants {
margin: 40px 20px; margin: 40px 20px;
justify-content: center; justify-content: center;

View File

@@ -136,7 +136,7 @@ export const Friend = observer(({ user }: Props) => {
})}> })}>
<UserIcon target={user} size={36} status /> <UserIcon target={user} size={36} status />
<div className={styles.name}> <div className={styles.name}>
<span>{user.display_name ?? user.username}</span> <span>{user.username}</span>
{subtext && <span className={styles.subtext}>{subtext}</span>} {subtext && <span className={styles.subtext}>{subtext}</span>}
</div> </div>
<div className={styles.actions}>{actions}</div> <div className={styles.actions}>{actions}</div>

View File

@@ -34,7 +34,7 @@
a { a {
width: 100%; width: 100%;
height: 6em;
div { div {
margin: 0; margin: 0;
} }
@@ -54,4 +54,3 @@
[data-light="true"] .home svg { [data-light="true"] .home svg {
filter: invert(100%); filter: invert(100%);
} }

View File

@@ -51,9 +51,9 @@ export default observer(() => {
state.settings.set("appearance:seasonal", !seasonalTheme); state.settings.set("appearance:seasonal", !seasonalTheme);
const isDecember = !isTouchscreenDevice && new Date().getMonth() === 11; const isDecember = !isTouchscreenDevice && new Date().getMonth() === 11;
const isOctober = !isTouchscreenDevice && new Date().getMonth() === 9; const isOctober = !isTouchscreenDevice && new Date().getMonth() === 9
const snowflakes = useMemo(() => { const snowflakes = useMemo(() => {
const flakes: string[] = []; const flakes = [];
if (isDecember) { if (isDecember) {
for (let i = 0; i < 15; i++) { for (let i = 0; i < 15; i++) {
@@ -175,7 +175,7 @@ export default observer(() => {
</CategoryButton> </CategoryButton>
</Link> </Link>
<a <a
href="https://wiki.revolt.chat/notes/project/financial-support/" href="https://insrt.uk/donate"
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

@@ -1,19 +1,17 @@
import { Twitter, Github, Mastodon } from "@styled-icons/boxicons-logos"; import { Twitter, Github, Mastodon } from "@styled-icons/boxicons-logos";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link, Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import styles from "./Login.module.scss"; import styles from "./Login.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import LocaleSelector from "../../components/common/LocaleSelector";
import wideSVG from "/assets/wide.svg"; import wideSVG from "/assets/wide.svg";
import LocaleSelector from "../../components/common/LocaleSelector";
import { Titlebar } from "../../components/native/Titlebar"; import { Titlebar } from "../../components/native/Titlebar";
import { useSystemAlert } from "../../updateWorker";
import { StatusBar } from "../RevoltApp";
import { FormCreate } from "./forms/FormCreate"; import { FormCreate } from "./forms/FormCreate";
import { FormLogin } from "./forms/FormLogin"; import { FormLogin } from "./forms/FormLogin";
import { FormReset, FormSendReset } from "./forms/FormReset"; import { FormReset, FormSendReset } from "./forms/FormReset";
@@ -23,31 +21,11 @@ export default observer(() => {
const state = useApplicationState(); const state = useApplicationState();
const theme = state.settings.theme; const theme = state.settings.theme;
const alert = useSystemAlert();
return ( return (
<> <>
{window.isNative && !window.native.getConfig().frame && ( {window.isNative && !window.native.getConfig().frame && (
<Titlebar overlay /> <Titlebar overlay />
)} )}
{alert && (
<StatusBar>
<div className="title">{alert.text}</div>
<div className="actions">
{alert.actions?.map((action) =>
action.type === "internal" ? null : action.type ===
"external" ? (
<a
href={action.href}
target="_blank"
rel="noreferrer">
<div className="button">{action.text}</div>{" "}
</a>
) : null,
)}
</div>
</StatusBar>
)}
<div className={styles.login}> <div className={styles.login}>
<Helmet> <Helmet>
<meta <meta
@@ -99,20 +77,17 @@ export default observer(() => {
<div className={styles.socials}> <div className={styles.socials}>
<a <a
href="https://github.com/revoltchat" href="https://github.com/revoltchat"
target="_blank" target="_blank" rel="noreferrer">
rel="noreferrer">
<Github size={24} /> <Github size={24} />
</a> </a>
<a <a
href="https://twitter.com/revoltchat" href="https://twitter.com/revoltchat"
target="_blank" target="_blank" rel="noreferrer">
rel="noreferrer">
<Twitter size={24} /> <Twitter size={24} />
</a> </a>
<a <a
href="https://mastodon.social/@revoltchat" href="https://mastodon.social/@revoltchat"
target="_blank" target="_blank" rel="noreferrer">
rel="noreferrer">
<Mastodon size={24} /> <Mastodon size={24} />
</a> </a>
</div> </div>
@@ -141,8 +116,7 @@ export default observer(() => {
<a <a
className={styles.attribution} className={styles.attribution}
href="https://unsplash.com/@fakurian" href="https://unsplash.com/@fakurian"
target="_blank" target="_blank" rel="noreferrer">
rel="noreferrer">
<Text id="general.image_by" /> &lrm;@fakurian &rlm;· <Text id="general.image_by" /> &lrm;@fakurian &rlm;·
unsplash.com unsplash.com
</a> </a>

View File

@@ -202,9 +202,11 @@ export const Form = observer(({ page, callback }: Props) => {
/> />
)} )}
{error && ( {error && (
<p style={{ fontSize: "0.8em", color: "var(--error)" }}> <Category>
<Text id={`error.${error}`} children={error} /> <I18nError error={error}>
</p> <Text id={`login.error.${page}`} />
</I18nError>
</Category>
)} )}
<Button> <Button>
<Text <Text
@@ -257,7 +259,7 @@ export const Form = observer(({ page, callback }: Props) => {
<span> <span>
<Text id="login.unofficial_instance" />{" "} <Text id="login.unofficial_instance" />{" "}
<a <a
href="https://developers.revolt.chat/faq.html" href="https://developers.revolt.chat/faq/instances#what-is-a-third-party-instance"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">

View File

@@ -1,7 +1,7 @@
import styles from "../Login.module.scss"; import styles from "../Login.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { Button, Tip } from "@revoltchat/ui"; import { Button } from "@revoltchat/ui";
interface Props { interface Props {
email?: string; email?: string;
@@ -115,15 +115,6 @@ export function MailProvider({ email }: Props) {
/> />
</Button> </Button>
</a> </a>
{provider[0] === "iCloud Mail" && (
<Tip palette="error">
<span>
iCloud users may not receive any emails due to a block
by Proofpoint. Please use a different email provider if
you do not receive anything.
</span>
</Tip>
)}
</div> </div>
); );
} }

View File

@@ -79,6 +79,7 @@ export default observer(() => {
id: "emojis", id: "emojis",
icon: <HappyBeaming size={20} />, icon: <HappyBeaming size={20} />,
title: <Text id="app.settings.server_pages.emojis.title" />, title: <Text id="app.settings.server_pages.emojis.title" />,
hidden: !state.experiments.isEnabled("picker"),
}, },
{ {
category: ( category: (

View File

@@ -80,15 +80,10 @@ const AccountHeader = styled.div`
font-size: 12px; font-size: 12px;
gap: 2px; gap: 2px;
.new { > span {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
} }
.full {
font-size: 14px;
font-weight: 600;
}
} }
} }
@@ -281,7 +276,7 @@ export default observer(() => {
</ButtonItem> </ButtonItem>
</a> </a>
<a <a
href="https://wiki.revolt.chat/notes/project/financial-support/" href="https://insrt.uk/donate"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<ButtonItem className={styles.donate} compact> <ButtonItem className={styles.donate} compact>
@@ -341,15 +336,7 @@ export default observer(() => {
onClick={() => openContextMenu("Status")} onClick={() => openContextMenu("Status")}
/> />
<div className="details"> <div className="details">
<span className="new"> <Username user={client.user!} prefixAt />
{client.user.display_name ??
client.user.username}
</span>
<span className="full">
{client.user.username}
{"#"}
{client.user.discriminator}
</span>
<UserStatus user={client.user!} /> <UserStatus user={client.user!} />
</div> </div>
</div> </div>

View File

@@ -94,33 +94,6 @@ export function Audio() {
return ( return (
<> <>
<div className={styles.audio}> <div className={styles.audio}>
<Tip palette="warning">
<span>
We are currently{" "}
<a
style={{ color: "inherit", fontWeight: "600" }}
href="https://github.com/revoltchat/frontend/issues/14"
target="_blank"
rel="noreferrer">
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>
</Tip>
{!permission && ( {!permission && (
<Tip palette="error"> <Tip palette="error">
<Text id="app.settings.pages.audio.tip_grant_permission" /> <Text id="app.settings.pages.audio.tip_grant_permission" />

View File

@@ -5,76 +5,60 @@ import { Link } from "react-router-dom";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { CategoryButton, Column, Tip } from "@revoltchat/ui"; import { CategoryButton } from "@revoltchat/ui";
export function Feedback() { export function Feedback() {
return ( return (
<Column> <div className={styles.feedback}>
<Tip palette="warning"> <a
<span> href="https://github.com/revoltchat/revolt/discussions"
We are currently rebuilding the client from scratch. You can{" "} target="_blank"
<a rel="noreferrer">
style={{ color: "inherit", fontWeight: "600" }} <CategoryButton
href="https://github.com/revoltchat/frontend/issues/14" action="external"
target="_blank" icon={<Github size={24} />}
rel="noreferrer"> description={
view our progress here <Text id="app.settings.pages.feedback.suggest_desc" />
</a> }>
; in the meantime, we are only accepting and fixing critical <Text id="app.settings.pages.feedback.suggest" />
bugs with the current client. </CategoryButton>
</span> </a>
</Tip> <a
<div className={styles.feedback}> href="https://github.com/revoltchat/revite/issues/new/choose"
<a target="_blank"
href="https://github.com/revoltchat/revolt/discussions" rel="noreferrer">
target="_blank" <CategoryButton
rel="noreferrer"> action="external"
icon={<ListOl size={24} />}
description={
<Text id="app.settings.pages.feedback.issue_desc" />
}>
<Text id="app.settings.pages.feedback.issue" />
</CategoryButton>
</a>
<a
href="https://github.com/orgs/revoltchat/projects/3"
target="_blank"
rel="noreferrer">
<CategoryButton
action="external"
icon={<BugAlt size={24} />}
description={
<Text id="app.settings.pages.feedback.bug_desc" />
}>
<Text id="app.settings.pages.feedback.bug" />
</CategoryButton>
</a>
<Link to="/invite/Testers">
<a>
<CategoryButton <CategoryButton
action="external" action="chevron"
icon={<Github size={24} />} icon={<Group size={24} />}
description={ description="You can report issues and discuss improvements with us directly here.">
<Text id="app.settings.pages.feedback.suggest_desc" /> {"Join the Revolt Lounge"}
}>
<Text id="app.settings.pages.feedback.suggest" />
</CategoryButton> </CategoryButton>
</a> </a>
<a </Link>
href="https://github.com/revoltchat/revite/issues/new/choose" </div>
target="_blank"
rel="noreferrer">
<CategoryButton
action="external"
icon={<ListOl size={24} />}
description={
<Text id="app.settings.pages.feedback.issue_desc" />
}>
<Text id="app.settings.pages.feedback.issue" />
</CategoryButton>
</a>
<a
href="https://github.com/orgs/revoltchat/projects/3"
target="_blank"
rel="noreferrer">
<CategoryButton
action="external"
icon={<BugAlt size={24} />}
description={
<Text id="app.settings.pages.feedback.bug_desc" />
}>
<Text id="app.settings.pages.feedback.bug" />
</CategoryButton>
</a>
<Link to="/invite/Testers">
<a>
<CategoryButton
action="chevron"
icon={<Group size={24} />}
description="You can report issues and discuss improvements with us directly here.">
{"Join the Revolt Lounge"}
</CategoryButton>
</a>
</Link>
</div>
</Column>
); );
} }

View File

@@ -35,7 +35,6 @@ import { modalController } from "../../../controllers/modals/ModalController";
interface Data { interface Data {
_id: string; _id: string;
token: string;
username: string; username: string;
public: boolean; public: boolean;
interactions_url?: string; interactions_url?: string;
@@ -75,7 +74,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
const [user, setUser] = useState<User>(client.users.get(bot._id)!); const [user, setUser] = useState<User>(client.users.get(bot._id)!);
const [data, setData] = useState<Data>({ const [data, setData] = useState<Data>({
_id: bot._id, _id: bot._id,
token: bot.token,
username: user.username, username: user.username,
public: bot.public, public: bot.public,
interactions_url: bot.interactions_url as any, interactions_url: bot.interactions_url as any,
@@ -96,7 +94,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
const refreshProfile = useCallback(() => { const refreshProfile = useCallback(() => {
client.api client.api
.get(`/users/${bot._id as ""}/profile`, undefined, { .get(`/users/${bot._id as ""}/profile`, undefined, {
headers: { "x-bot-token": data.token }, headers: { "x-bot-token": bot.token },
}) })
.then((profile) => setProfile(profile ?? {})); .then((profile) => setProfile(profile ?? {}));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -151,7 +149,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
"/users/@me", "/users/@me",
avatar ? { avatar } : { remove: ["Avatar"] }, avatar ? { avatar } : { remove: ["Avatar"] },
{ {
headers: { "x-bot-token": data.token }, headers: { "x-bot-token": bot.token },
}, },
); );
@@ -170,7 +168,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
? { profile: { background } } ? { profile: { background } }
: { remove: ["ProfileBackground"] }, : { remove: ["ProfileBackground"] },
{ {
headers: { "x-bot-token": data.token }, headers: { "x-bot-token": bot.token },
}, },
); );
@@ -186,7 +184,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
"/users/@me", "/users/@me",
content ? { profile: { content } } : { remove: ["ProfileContent"] }, content ? { profile: { content } } : { remove: ["ProfileContent"] },
{ {
headers: { "x-bot-token": data.token }, headers: { "x-bot-token": bot.token },
}, },
); );
@@ -314,7 +312,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
if (editMode) { if (editMode) {
setData({ setData({
_id: bot._id, _id: bot._id,
token: data.token,
username: user!.username, username: user!.username,
public: bot.public, public: bot.public,
interactions_url: bot.interactions_url as any, interactions_url: bot.interactions_url as any,
@@ -337,7 +334,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
<CategoryButton <CategoryButton
account account
icon={<Key size={24} />} icon={<Key size={24} />}
onClick={() => modalController.writeText(data.token)} onClick={() => modalController.writeText(bot.token)}
description={ description={
<> <>
{"••••• "} {"••••• "}
@@ -347,7 +344,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
ev, ev,
modalController.push({ modalController.push({
type: "show_token", type: "show_token",
token: data.token, token: bot.token,
name: user!.username, name: user!.username,
}), }),
) )
@@ -493,25 +490,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
}> }>
<Text id="app.settings.pages.bots.add" /> <Text id="app.settings.pages.bots.add" />
</Button> </Button>
<Button
palette="error"
onClick={ async () => {
modalController.push({
type: "reset_bot_token",
target: { name: user.username, id: bot._id },
callback: async () => {
const updatedBot = await client.bots.fetch(bot._id);
setData({
...data,
token: updatedBot.bot.token
})
}
})
}
}>
<Text id="app.settings.pages.bots.reset_token" />
</Button>
</> </>
)} )}
</div> </div>

View File

@@ -66,7 +66,7 @@ export function Native() {
title="Enable Discord status" title="Enable Discord status"
description="Rep Revolt on your Discord status." description="Rep Revolt on your Discord status."
/> />
{/* <Checkbox <Checkbox
value={config.build === "nightly"} value={config.build === "nightly"}
onChange={(nightly) => { onChange={(nightly) => {
const build = nightly ? "nightly" : "stable"; const build = nightly ? "nightly" : "stable";
@@ -79,7 +79,7 @@ export function Native() {
}} }}
title="Revolt Nightly" title="Revolt Nightly"
description="Use the beta branch of Revolt." description="Use the beta branch of Revolt."
/> */} />
<h3>Titlebar</h3> <h3>Titlebar</h3>
<Checkbox <Checkbox
@@ -144,7 +144,7 @@ export function Native() {
</Button> </Button>
</p> </p>
<h3 style={{ marginTop: "4em" }}>Local Development Mode</h3> <h3 style={{ marginTop: "4em" }}>Local Development Mode</h3>
{/*config.build === "dev" ? ( {config.build === "dev" ? (
<> <>
<h5>Development mode is currently on.</h5> <h5>Development mode is currently on.</h5>
<Button <Button
@@ -197,7 +197,7 @@ export function Native() {
</Button> </Button>
</p> </p>
</> </>
)*/} )}
<hr /> <hr />
<CategoryButton <CategoryButton
icon={<img src={RLogo} draggable={false} />} icon={<img src={RLogo} draggable={false} />}

View File

@@ -694,23 +694,3 @@
section { section {
margin-bottom: 20px; margin-bottom: 20px;
} }
.titleNew {
display: flex;
margin: 15px 0;
gap: 8px;
align-items: center;
font-size: 0.8125rem;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary-foreground);
.new {
font-size: 10px;
background: var(--accent);
padding: 3px 5px;
border-radius: 20px;
color: white;
}
}

View File

@@ -1,5 +1,4 @@
import { Markdown } from "@styled-icons/boxicons-logos"; import { Markdown } from "@styled-icons/boxicons-logos";
import { UserCircle } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { API } from "revolt.js"; import { API } from "revolt.js";
@@ -8,7 +7,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { Button, LineDivider, Tip, CategoryButton } from "@revoltchat/ui"; import { Button, LineDivider, Tip } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useTranslation } from "../../../lib/i18n"; import { useTranslation } from "../../../lib/i18n";
@@ -18,7 +17,6 @@ import AutoComplete, {
} from "../../../components/common/AutoComplete"; } from "../../../components/common/AutoComplete";
import { useSession } from "../../../controllers/client/ClientController"; import { useSession } from "../../../controllers/client/ClientController";
import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads"; import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads";
import { modalController } from "../../../controllers/modals/ModalController";
import { UserProfile } from "../../../controllers/modals/components/legacy/UserProfile"; import { UserProfile } from "../../../controllers/modals/components/legacy/UserProfile";
export const Profile = observer(() => { export const Profile = observer(() => {
@@ -79,19 +77,6 @@ export const Profile = observer(() => {
{...({} as any)} {...({} as any)}
/> />
</div> </div>
<div className={styles.titleNew}>
Display Name
<div className={styles.new}>NEW</div>
</div>
<CategoryButton
onClick={() =>
modalController.push({ type: "modify_displayname" })
}
icon={<UserCircle size={24} />}
action="chevron"
description={"Change your display name to whatever you like"}>
Display Name
</CategoryButton>
{/*<h3>Badges</h3> {/*<h3>Badges</h3>
<div className={styles.badgePicker}> <div className={styles.badgePicker}>
<div className={styles.overlay} /> <div className={styles.overlay} />

View File

@@ -38,9 +38,7 @@ dayjs.extend(relativeTime);
export function Sessions() { export function Sessions() {
const client = useClient(); const client = useClient();
const deviceId = const deviceId =
typeof client.session === "object" typeof client.session === "object" ? client.session._id : undefined;
? (client.session as unknown as { _id: string })._id
: undefined;
const [sessions, setSessions] = useState<API.SessionInfo[] | undefined>( const [sessions, setSessions] = useState<API.SessionInfo[] | undefined>(
undefined, undefined,

View File

@@ -340,13 +340,11 @@ function ListElement({
const save = useCallback(() => { const save = useCallback(() => {
setEditing(undefined); setEditing(undefined);
if (editing !== "") { setTitle!(editing!);
setTitle!(editing!);
}
}, [editing, setTitle]); }, [editing, setTitle]);
useEffect(() => { useEffect(() => {
if (editing === undefined) return; if (!editing) return;
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
if ((ev.target as HTMLElement)?.id !== category.id) { if ((ev.target as HTMLElement)?.id !== category.id) {
@@ -369,10 +367,8 @@ function ListElement({
<KanbanList last={false} key={category.id}> <KanbanList last={false} key={category.id}>
<div className="inner"> <div className="inner">
<Row> <Row>
<KanbanListHeader <KanbanListHeader {...provided.dragHandleProps}>
{...provided.dragHandleProps} {editing ? (
onClick={startEditing}>
{editing !== undefined ? (
<input <input
value={editing} value={editing}
onChange={(e) => onChange={(e) =>
@@ -386,7 +382,7 @@ function ListElement({
id={category.id} id={category.id}
/> />
) : ( ) : (
<span> <span onClick={startEditing}>
{category.title} {category.title}
</span> </span>
)} )}

View File

@@ -4,7 +4,7 @@ import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { Button, Column, Row, Stacked, Tip } from "@revoltchat/ui"; import { Button, Column, Row, Stacked } from "@revoltchat/ui";
import UserShort from "../../../components/common/user/UserShort"; import UserShort from "../../../components/common/user/UserShort";
import { EmojiUploader } from "../../../components/settings/customisation/EmojiUploader"; import { EmojiUploader } from "../../../components/settings/customisation/EmojiUploader";
@@ -33,22 +33,6 @@ export const Emojis = observer(({ server }: Props) => {
return ( return (
<Column> <Column>
<Tip palette="warning">
<span>
This UI was never finished and will be polished in the{" "}
<a
style={{ color: "inherit", fontWeight: "600" }}
href="https://github.com/revoltchat/frontend/issues/14"
target="_blank"
rel="noreferrer">
new client
</a>
.<br />
<br />
Also please note that emoji names must be lowercase
alphanumeric!
</span>
</Tip>
{server.havePermission("ManageCustomisation") && ( {server.havePermission("ManageCustomisation") && (
<EmojiUploader server={server} /> <EmojiUploader server={server} />
)} )}

View File

@@ -103,43 +103,10 @@ export const Members = ({ server }: Props) => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
useEffect(() => { useEffect(() => {
function fetch() { server
server .fetchMembers()
.fetchMembers() .then((data) => data.members)
.then((data) => data.members) .then(setData);
.then(setData);
}
fetch();
// Members may be invalidated if we stop receiving events
// This is not very accurate, this should be tracked within
// revolt.js so we know the true validity.
let valid = true,
invalidationTimer: number;
function waitToInvalidate() {
invalidationTimer = setTimeout(() => {
valid = false;
}, 15 * 60e3) as never; // 15 minutes
}
function cancelInvalidation() {
if (!valid) {
fetch();
valid = true;
}
clearTimeout(invalidationTimer);
}
addEventListener("blur", waitToInvalidate);
addEventListener("focus", cancelInvalidation);
return () => {
removeEventListener("blur", waitToInvalidate);
removeEventListener("focus", cancelInvalidation);
};
}, [server, setData]); }, [server, setData]);
const members = useMemo( const members = useMemo(

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);
@@ -267,223 +70,213 @@ export const Roles = observer(({ server }: Props) => {
} }
`; `;
const DeleteRoleButton = styled(Button)`
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} />
</Tooltip>
<Tooltip
content={
<Text id="app.special.copy" />
}>
<a
onClick={() =>
modalController.writeText(
currentRole.id,
)
}>
{currentRole.id}
</a>
</Tooltip>
</RoleId>
</section>
<section>
<Category>
<Text id="app.settings.permissions.role_colour" />
</Category>
<p>
<ColourSwatches
value={
currentRoleValue.colour ??
"gray"
}
onChange={(colour) =>
setValue({ ...value, colour })
}
/>
</p>
</section>
<section>
<Category>
<Text id="app.settings.permissions.role_options" />
</Category>
<p>
<Checkbox
value={
currentRoleValue.hoist ?? false
}
onChange={(hoist) =>
setValue({ ...value, hoist })
}
title={
<Text id="app.settings.permissions.hoist_role" />
}
description={
<Text id="app.settings.permissions.hoist_desc" />
}
/>
</p>
</section>
<section>
<Category>
<Text id="app.settings.permissions.role_ranking" />
</Category>
<p>
<InputBox
type="number"
value={currentRoleValue.rank ?? 0}
onChange={(e) =>
setValue({
...value,
rank: parseInt(
e.currentTarget.value,
),
})
}
palette="secondary"
/>
</p>
</section>
</>
)}
<h1>
<Text id="app.settings.permissions.edit_title" />
</h1>
<PermissionList
value={currentRoleValue.permissions}
onChange={(permissions) =>
setValue({
...value,
permissions,
} as RoleOrDefault)
}
target={server}
/>
{selected !== "default" && (
<>
<hr />
<h1>
<Text id="app.settings.categories.danger_zone" />
</h1>
<Button <Button
palette="secondary" palette="error"
disabled={isEqual( compact
currentRole, onClick={deleteRole}>
currentRoleValue, <Text id="app.settings.permissions.delete_role" />
)}
onClick={save}>
<Text id="app.special.modals.actions.save" />
</Button> </Button>
</SpaceBetween> </>
<hr /> )}
{selected !== "default" && ( </div>
<> );
<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} />
</Tooltip>
<Tooltip
content={
<Text id="app.special.copy" />
}>
<a
onClick={() =>
modalController.writeText(
currentRole.id,
)
}>
{currentRole.id}
</a>
</Tooltip>
</RoleId>
</section>
<section>
<Category>
<Text id="app.settings.permissions.role_colour" />
</Category>
<p>
<ColourSwatches
value={
currentRoleValue.colour ??
"gray"
}
onChange={(colour) =>
setValue({
...value,
colour,
})
}
/>
</p>
</section>
<section>
<Category>
<Text id="app.settings.permissions.role_options" />
</Category>
<p>
<Checkbox
value={
currentRoleValue.hoist ??
false
}
onChange={(hoist) =>
setValue({
...value,
hoist,
})
}
title={
<Text id="app.settings.permissions.hoist_role" />
}
description={
<Text id="app.settings.permissions.hoist_desc" />
}
/>
</p>
</section>
</>
)}
<h1>
<Text id="app.settings.permissions.edit_title" />
</h1>
<PermissionList
value={currentRoleValue.permissions}
onChange={(permissions) =>
setValue({
...value,
permissions,
} as RoleOrDefault)
}
target={server}
/>
{selected !== "default" && (
<>
<hr />
<h1>
<Text id="app.settings.categories.danger_zone" />
</h1>
<DeleteRoleButton
palette="error"
compact
onClick={deleteRole}>
<Text id="app.settings.permissions.delete_role" />
</DeleteRoleButton>
</>
)}
</div>
);
}}
/>
</div>
); );
}); });

View File

@@ -78,7 +78,7 @@ export function useSystemAlert() {
*/ */
async function checkVersion() { async function checkVersion() {
const { version, poll_rate, alert } = (await fetch( const { version, poll_rate, alert } = (await fetch(
"https://health.revolt.chat/api/health", "https://api.revolt.chat/release",
).then((res) => res.json())) as { ).then((res) => res.json())) as {
version: string; version: string;
poll_rate?: number; poll_rate?: number;
@@ -111,10 +111,7 @@ async function checkVersion() {
} }
} }
if ( if (import.meta.env.VITE_API_URL === "https://api.revolt.chat") {
import.meta.env.VITE_API_URL === "https://api.revolt.chat" ||
import.meta.env.VITE_API_URL === "https://app.revolt.chat/api"
) {
// Check for critical updates hourly // Check for critical updates hourly
schedule(); schedule();
checkVersion(); checkVersion();

View File

@@ -14,7 +14,6 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "preact",
"jsxFactory": "h", "jsxFactory": "h",
"jsxFragmentFactory": "Fragment", "jsxFragmentFactory": "Fragment",
"types": ["vite-plugin-pwa/client"], "types": ["vite-plugin-pwa/client"],

159
yarn.lock
View File

@@ -2299,20 +2299,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@revoltchat/rehype-katex@npm:6.0.3-patch.1":
version: 6.0.3-patch.1
resolution: "@revoltchat/rehype-katex@npm:6.0.3-patch.1"
dependencies:
"@types/hast": ^2.0.0
"@types/katex": ^0.14.0
hast-util-from-html-isomorphic: ^1.0.0
hast-util-to-text: ^3.1.0
katex: ^0.16.7
unist-util-visit: ^4.0.0
checksum: 1b7e57ddafa300e96396a5f5d64dad947c201b514562b1d581ac1a683252bc074fcb620fb271c136dd8b84c069dad23cd98ef0b01585be426bb2ce3f5dde004f
languageName: node
linkType: hard
"@revoltchat/ui@portal:external/components::locator=client%40workspace%3A.": "@revoltchat/ui@portal:external/components::locator=client%40workspace%3A.":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@revoltchat/ui@portal:external/components::locator=client%40workspace%3A." resolution: "@revoltchat/ui@portal:external/components::locator=client%40workspace%3A."
@@ -2641,13 +2627,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/katex@npm:^0.14.0":
version: 0.14.0
resolution: "@types/katex@npm:0.14.0"
checksum: 330e0d0337ba48c87f5b793965fbad673653789bf6e50dfe8d726a7b0cbefd37195055e31503aae629814aa79447e4f23a4b87ad1ac565c0d9a9d9978836f39b
languageName: node
linkType: hard
"@types/lodash.defaultsdeep@npm:^4.6.6": "@types/lodash.defaultsdeep@npm:^4.6.6":
version: 4.6.6 version: 4.6.6
resolution: "@types/lodash.defaultsdeep@npm:4.6.6" resolution: "@types/lodash.defaultsdeep@npm:4.6.6"
@@ -3709,7 +3688,6 @@ __metadata:
"@hcaptcha/react-hcaptcha": ^1.4.4 "@hcaptcha/react-hcaptcha": ^1.4.4
"@insertish/vite-plugin-babel-macros": ^1.0.5 "@insertish/vite-plugin-babel-macros": ^1.0.5
"@preact/preset-vite": ^2.0.0 "@preact/preset-vite": ^2.0.0
"@revoltchat/rehype-katex": 6.0.3-patch.1
"@revoltchat/ui": ^1.0.77 "@revoltchat/ui": ^1.0.77
"@rollup/plugin-replace": ^2.4.2 "@rollup/plugin-replace": ^2.4.2
"@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-logos": ^10.38.0
@@ -3754,7 +3732,6 @@ __metadata:
lodash.defaultsdeep: ^4.6.1 lodash.defaultsdeep: ^4.6.1
lodash.isequal: ^4.5.0 lodash.isequal: ^4.5.0
long: ^5.2.0 long: ^5.2.0
lottie-react: ^2.4.0
mdast-util-to-hast: ^12.1.2 mdast-util-to-hast: ^12.1.2
mediasoup-client: "npm:@insertish/mediasoup-client@3.6.36-esnext" mediasoup-client: "npm:@insertish/mediasoup-client@3.6.36-esnext"
mobx: ^6.6.0 mobx: ^6.6.0
@@ -3773,6 +3750,7 @@ __metadata:
react-router-dom: ^5.2.0 react-router-dom: ^5.2.0
react-scroll: ^1.8.2 react-scroll: ^1.8.2
react-virtuoso: ^2.12.0 react-virtuoso: ^2.12.0
rehype-katex: ^6.0.2
rehype-prism: ^2.1.3 rehype-prism: ^2.1.3
rehype-react: ^7.1.1 rehype-react: ^7.1.1
remark-breaks: ^3.0.2 remark-breaks: ^3.0.2
@@ -3897,7 +3875,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"commander@npm:^8.0.0, commander@npm:^8.3.0": "commander@npm:^8.0.0":
version: 8.3.0 version: 8.3.0
resolution: "commander@npm:8.3.0" resolution: "commander@npm:8.3.0"
checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0
@@ -4254,13 +4232,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"entities@npm:^4.4.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7
languageName: node
linkType: hard
"env-paths@npm:^2.2.0": "env-paths@npm:^2.2.0":
version: 2.2.1 version: 2.2.1
resolution: "env-paths@npm:2.2.1" resolution: "env-paths@npm:2.2.1"
@@ -5353,41 +5324,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"hast-util-from-dom@npm:^4.0.0":
version: 4.2.0
resolution: "hast-util-from-dom@npm:4.2.0"
dependencies:
hastscript: ^7.0.0
web-namespaces: ^2.0.0
checksum: 0eac72cfb2bad20cf70ad978332be8c746dba27576fea3c3b123d7d1a52a8e10a88a25622b60f45255994163845e3f784fcd7c013b1301f2df4b81e3d0b1b973
languageName: node
linkType: hard
"hast-util-from-html-isomorphic@npm:^1.0.0":
version: 1.0.0
resolution: "hast-util-from-html-isomorphic@npm:1.0.0"
dependencies:
"@types/hast": ^2.0.0
hast-util-from-dom: ^4.0.0
hast-util-from-html: ^1.0.0
unist-util-remove-position: ^4.0.0
checksum: a72786d6757a1a38d76cf74ba86fecfaf0690d465dcae477bfa7199ec03d364ba964f658331406ee7e62e912186df6d4ff38fc3ad050e0e3d8bc33a653df060d
languageName: node
linkType: hard
"hast-util-from-html@npm:^1.0.0":
version: 1.0.2
resolution: "hast-util-from-html@npm:1.0.2"
dependencies:
"@types/hast": ^2.0.0
hast-util-from-parse5: ^7.0.0
parse5: ^7.0.0
vfile: ^5.0.0
vfile-message: ^3.0.0
checksum: 81cbda7dfa4c02fcb4e4359a48e25ebee7bef133dc607c2c036c53df27efacdfa22313a86398391ef22b31decda98c7110f1b441c5a48f419514be6b5d4b5603
languageName: node
linkType: hard
"hast-util-from-parse5@npm:^7.0.0": "hast-util-from-parse5@npm:^7.0.0":
version: 7.1.0 version: 7.1.0
resolution: "hast-util-from-parse5@npm:7.1.0" resolution: "hast-util-from-parse5@npm:7.1.0"
@@ -5405,12 +5341,12 @@ __metadata:
linkType: hard linkType: hard
"hast-util-is-element@npm:^2.0.0": "hast-util-is-element@npm:^2.0.0":
version: 2.1.3 version: 2.1.2
resolution: "hast-util-is-element@npm:2.1.3" resolution: "hast-util-is-element@npm:2.1.2"
dependencies: dependencies:
"@types/hast": ^2.0.0 "@types/hast": ^2.0.0
"@types/unist": ^2.0.0 "@types/unist": ^2.0.0
checksum: 9d988f6839a50566a895a3dd19222e6ab1591243f6a3c36bba835b7e9339a2845f1ff1c583425afd602de1a57a76c5bae8a6dc0ab1d6e5d1e252b422cdeadbb7 checksum: c5fe9f7cde3775d4cbe19a9a55631a80b7a4ea0131fc2e3d097ebe228a35f09b9219f64b788b7a9cf819e6dcb6d1fc7830fd2f10ad536649e436e8c83da41e00
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5424,14 +5360,13 @@ __metadata:
linkType: hard linkType: hard
"hast-util-to-text@npm:^3.1.0": "hast-util-to-text@npm:^3.1.0":
version: 3.1.2 version: 3.1.1
resolution: "hast-util-to-text@npm:3.1.2" resolution: "hast-util-to-text@npm:3.1.1"
dependencies: dependencies:
"@types/hast": ^2.0.0 "@types/hast": ^2.0.0
"@types/unist": ^2.0.0
hast-util-is-element: ^2.0.0 hast-util-is-element: ^2.0.0
unist-util-find-after: ^4.0.0 unist-util-find-after: ^4.0.0
checksum: d17cf3344c1d584ddd811cbb78d25b6c9819e62c8edb9643b53be38083fd978a6fa9a5bf6e6cd7b5ea48d30d9edc2859acae40b8bb89e166bebcda6017d4703d checksum: 2312a818c8ec7b02307b04175357e5a7a9918f48624d05366668ba60918734ca62b0ee21006a2a448e0e5a198654cd1fa4ba8c813702b465cb487e2320db523a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6069,14 +6004,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"katex@npm:^0.16.7": "katex@npm:^0.15.0":
version: 0.16.7 version: 0.15.6
resolution: "katex@npm:0.16.7" resolution: "katex@npm:0.15.6"
dependencies: dependencies:
commander: ^8.3.0 commander: ^8.0.0
bin: bin:
katex: cli.js katex: cli.js
checksum: 6c3f61e28820ecba074a149d17be6d731d8a950cdd7826851e16f2b0f3627c4119acd8c6e4fb86950063c4e99a6e478720ea6cca3dc82e59bbca81001ee00b70 checksum: 2da808bbd1d3be27715006cd86767dd3fcce3e317fb3bbd64d407328d2d90de17b5d83062b2cfd0e0d0de32e340efbac214862bc96892a5d1492462e553728d4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6270,25 +6205,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lottie-react@npm:^2.4.0":
version: 2.4.0
resolution: "lottie-react@npm:2.4.0"
dependencies:
lottie-web: ^5.10.2
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: e9ea4a89be90a29bde4a83956f76a80d1f8031882f18ea38ef5271d2aafd8e68348ae6297f185ed85b149ca4896fe33aee7faf9780b88a1b289b8e146f477448
languageName: node
linkType: hard
"lottie-web@npm:^5.10.2":
version: 5.12.0
resolution: "lottie-web@npm:5.12.0"
checksum: 77c35be880e484d1a766f21f789ede7c7be59e957579580a18c2dfec08c4812c03e1d652e453166c31fd80556446d6b857fb490d901e34154105532519bc5ef5
languageName: node
linkType: hard
"lru-cache@npm:^6.0.0": "lru-cache@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "lru-cache@npm:6.0.0" resolution: "lru-cache@npm:6.0.0"
@@ -7352,15 +7268,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse5@npm:^7.0.0":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
dependencies:
entities: ^4.4.0
checksum: 59465dd05eb4c5ec87b76173d1c596e152a10e290b7abcda1aecf0f33be49646ea74840c69af975d7887543ea45564801736356c568d6b5e71792fd0f4055713
languageName: node
linkType: hard
"path-exists@npm:^4.0.0": "path-exists@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "path-exists@npm:4.0.0" resolution: "path-exists@npm:4.0.0"
@@ -7887,7 +7794,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rehype-parse@npm:^7 || ^ 8": "rehype-katex@npm:^6.0.2":
version: 6.0.2
resolution: "rehype-katex@npm:6.0.2"
dependencies:
"@types/hast": ^2.0.0
"@types/katex": ^0.11.0
hast-util-to-text: ^3.1.0
katex: ^0.15.0
rehype-parse: ^8.0.0
unified: ^10.0.0
unist-util-remove-position: ^4.0.0
unist-util-visit: ^4.0.0
checksum: ac8b3486441697b8e22cb7ebf6ec58e06d190240f45b128fe60422b9eb887599f38406581e6e3356af967eb1d45d631b0c09387f060190641f402f56c78fa771
languageName: node
linkType: hard
"rehype-parse@npm:^7 || ^ 8, rehype-parse@npm:^8.0.0":
version: 8.0.4 version: 8.0.4
resolution: "rehype-parse@npm:8.0.4" resolution: "rehype-parse@npm:8.0.4"
dependencies: dependencies:
@@ -8093,14 +8016,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"revolt-api@npm:0.6.0": "revolt-api@npm:0.5.5-4":
version: 0.6.0 version: 0.5.5-4
resolution: "revolt-api@npm:0.6.0" resolution: "revolt-api@npm:0.5.5-4"
dependencies: dependencies:
"@insertish/oapi": 0.1.18 "@insertish/oapi": 0.1.18
axios: ^0.26.1 axios: ^0.26.1
lodash.defaultsdeep: ^4.6.1 lodash.defaultsdeep: ^4.6.1
checksum: 94865aa1aee5b05682ffe4e40a0fe431809f879437399943fc429d078160a5fe24686011b43639d6fae88e9a7a43fbe4bb9beb9bbe1bb3070965636f0ec051fd checksum: dfb374d58f1b8b5a6de2e7fa05e386b3df3ffb85a450a6894f23c6b9760af8bff0d198d8352063c33084f0dbc6ff84a234300efb0c62c7b27f709887402787f1
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8117,7 +8040,7 @@ __metadata:
lodash.isequal: ^4.5.0 lodash.isequal: ^4.5.0
long: ^5.2.0 long: ^5.2.0
mobx: ^6.3.2 mobx: ^6.3.2
revolt-api: 0.6.0 revolt-api: 0.5.5-4
ulid: ^2.3.0 ulid: ^2.3.0
ws: ^8.2.2 ws: ^8.2.2
languageName: node languageName: node
@@ -9197,12 +9120,12 @@ __metadata:
linkType: hard linkType: hard
"unist-util-find-after@npm:^4.0.0": "unist-util-find-after@npm:^4.0.0":
version: 4.0.1 version: 4.0.0
resolution: "unist-util-find-after@npm:4.0.1" resolution: "unist-util-find-after@npm:4.0.0"
dependencies: dependencies:
"@types/unist": ^2.0.0 "@types/unist": ^2.0.0
unist-util-is: ^5.0.0 unist-util-is: ^5.0.0
checksum: bed7e7a1a87539bea0b33ddc9ce8e2f3fdd4a7c87e143a848ed5bbb4cf9c563ade7ecf80b3ee5a38f9ad9e6af29cdb8cdde9001eea92542cbb14784f5add7019 checksum: 8381ef0bad18a0b1fa1c7ee47f94a2578ab6bf572eb126a1f179526b9dca47584fc070976f2d83bbe381161fa33b9164a894d0279a30ec83e65433356d43df57
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9237,12 +9160,12 @@ __metadata:
linkType: hard linkType: hard
"unist-util-remove-position@npm:^4.0.0": "unist-util-remove-position@npm:^4.0.0":
version: 4.0.2 version: 4.0.1
resolution: "unist-util-remove-position@npm:4.0.2" resolution: "unist-util-remove-position@npm:4.0.1"
dependencies: dependencies:
"@types/unist": ^2.0.0 "@types/unist": ^2.0.0
unist-util-visit: ^4.0.0 unist-util-visit: ^4.0.0
checksum: 989831da913d09a82a99ed9b47b78471b6409bde95942cde47e09da54b7736516f17e3c7e026af468684c1efcec5fb52df363381b2f9dc7fd96ce791c5a2fa4a checksum: 7d2808662ac65f2b2f615822b78060419f738fb3b074b10cec77c596ea966b8f5c47553d2d322822a5975c49d2b21cdd64c198ae9fb02a9d54d1afa6342cdd6a
languageName: node languageName: node
linkType: hard linkType: hard