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
108 changed files with 1050 additions and 2787 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

1
.envrc
View File

@@ -1 +0,0 @@
use flake

66
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Bug report
description: File a bug report
title: "bug: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
validations:
required: true
- type: dropdown
id: branch
attributes:
label: Branch
description: What branch of Revolt are you using?
options:
- Production (app.revolt.chat)
- Nightly (nightly.revolt.chat)
validations:
required: true
- type: textarea
id: commit-hash
attributes:
label: Commit hash
description: What is your commit hash? You can find this at the bottom of Settings, next to the branch name.
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other (please specify in the "What happened" form)
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. (To get this, press `CTRL`- or `CMD`-`SHIFT`-`I` and navigate to the "Console" tab.)
render: shell
- type: checkboxes
id: desktop
attributes:
label: Desktop
description: Is this bug specific to [the desktop client](https://github.com/revoltchat/desktop)? (If not, leave this unchecked.)
options:
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
required: false
- type: checkboxes
id: pwa
attributes:
label: PWA
description: Is this bug specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
options:
- label: Yes, this bug is specific to the PWA.
required: false

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
contact_links:
- name: Lounge Chat
url: https://rvlt.gg/Testers
about: Ask questions and discuss with others.
- name: Discussions
url: https://github.com/orgs/revoltchat/discussions
about: For larger feature requests and general question & answer.

24
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Feature request
description: Make a feature request
title: "feature request: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Before you start, a lot of bigger features may be better suited as [API issues](https://github.com/revoltchat/delta/issues/new) or [centralised discussions](https://github.com/revoltchat/revolt/discussions/new).
- type: textarea
id: your-idea
attributes:
label: What do you want to see?
description: Describe your idea in as much detail as possible - if applicable, screenshots/mockups are really useful.
validations:
required: true
- type: checkboxes
id: pwa
attributes:
label: PWA
description: Is this feature request specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
options:
- label: Yes, this feature request is specific to the PWA.
required: false

7
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,7 @@
## Please make sure to check the following tasks before opening and submitting a PR
* [ ] 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
* [ ] 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)
* [ ] I have included screenshots to demonstrate my changes

View File

@@ -1,44 +1,111 @@
name: Docker name: Docker
on: on:
push: push:
branches: branches:
- "handmade" - "master"
pull_request: tags:
branches: - "*"
- "handmade" paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
pull_request:
branches:
- "master"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
workflow_dispatch:
jobs: jobs:
publish: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
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 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:
needs: [test]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
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 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: handmadecities/handmade-revolt-web-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
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
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 }}

49
.github/workflows/triage_issue.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Add Issue to Board
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.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.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

72
.github/workflows/triage_pr.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.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.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectNextItemField(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: $status_value
}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

2
.gitignore vendored
View File

@@ -15,5 +15,3 @@ public/assets_*
!public/assets_default !public/assets_default
.vscode/chrome_data .vscode/chrome_data
.direnv

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,27 +1,3 @@
# Handmade Revolt
Fork of Revolt (now Stoat chat) maintained by the handmade cities community.
## 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

64
flake.lock generated
View File

@@ -1,64 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770169770,
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "aa290c9891fa4ebe88f8889e59633d20cc06a5f2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,28 +0,0 @@
{
description = "Node+yarn dev shell flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
flake-utils = {
url = "github:numtide/flake-utils";
inputs.systems.follows = "systems";
};
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs_24
yarn
];
};
}
);
}

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

@@ -16,11 +16,6 @@ import { IconButton } from "@revoltchat/ui";
import { emojiDictionary } from "../../../../assets/emojis"; import { emojiDictionary } from "../../../../assets/emojis";
import { useClient } from "../../../../controllers/client/ClientController"; import { useClient } from "../../../../controllers/client/ClientController";
import { modalController } from "../../../../controllers/modals/ModalController";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import Tooltip from "../../Tooltip";
import UserIcon from "../../user/UserIcon";
import { Username } from "../../user/UserShort";
import { RenderEmoji } from "../../../markdown/plugins/emoji"; import { RenderEmoji } from "../../../markdown/plugins/emoji";
import { HackAlertThisFileWillBeReplaced } from "../MessageBox"; import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
@@ -90,38 +85,6 @@ const Reaction = styled.div<{ active: boolean }>`
`} `}
`; `;
/**
* Tooltip content for reaction users
*/
const TooltipUserList = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
max-width: 200px;
`;
const TooltipUserRow = styled.div`
display: flex;
align-items: center;
gap: 6px;
font-size: 0.9em;
padding: 4px 6px;
margin: -4px -6px;
border-radius: var(--border-radius);
cursor: pointer;
&:hover {
background: var(--secondary-background);
}
`;
const TooltipMoreText = styled.div`
font-size: 0.85em;
color: var(--secondary-foreground);
padding-top: 2px;
`;
/** /**
* Render reactions on a message * Render reactions on a message
*/ */
@@ -135,97 +98,16 @@ export const Reactions = observer(({ message }: Props) => {
const Entry = useCallback( const Entry = useCallback(
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => { observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
const active = user_ids?.has(client.user!._id) || false; const active = user_ids?.has(client.user!._id) || false;
const userIds = user_ids ? Array.from(user_ids) : [];
const longPressTimer = useRef<number | null>(null);
const didLongPress = useRef(false);
const handleClick = () => { return (
if (didLongPress.current) {
didLongPress.current = false;
return;
}
active ? message.unreact(id) : message.react(id);
};
const openModal = () => {
modalController.push({
type: "reaction_users",
emoji: id,
userIds,
});
};
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault();
e.stopPropagation();
didLongPress.current = false;
longPressTimer.current = window.setTimeout(() => {
didLongPress.current = true;
openModal();
}, 500);
};
const handleTouchEnd = (e: TouchEvent) => {
e.stopPropagation();
if (longPressTimer.current) {
clearTimeout(longPressTimer.current);
longPressTimer.current = null;
// Short tap - trigger manually since preventDefault blocks click
if (!didLongPress.current) {
active ? message.unreact(id) : message.react(id);
}
}
didLongPress.current = false;
};
const openUserProfile = (userId: string) => {
modalController.push({ type: "user_profile", user_id: userId });
};
// Build tooltip content showing up to 10 users
const tooltipContent = (
<TooltipUserList>
{userIds.slice(0, 10).map((userId) => {
const user = client.users.get(userId);
return (
<TooltipUserRow
key={userId}
onClick={() => openUserProfile(userId)}>
<UserIcon target={user} size={18} />
<Username user={user} />
</TooltipUserRow>
);
})}
{userIds.length > 10 && (
<TooltipMoreText>
and {userIds.length - 10} more...
</TooltipMoreText>
)}
</TooltipUserList>
);
const reactionElement = (
<Reaction <Reaction
active={active} active={active}
onClick={handleClick} onClick={() =>
onTouchStart={isTouchscreenDevice ? handleTouchStart : undefined} active ? message.unreact(id) : message.react(id)
onTouchEnd={isTouchscreenDevice ? handleTouchEnd : undefined} }>
onTouchCancel={isTouchscreenDevice ? handleTouchEnd : undefined}>
<RenderEmoji match={id} /> {user_ids?.size || 0} <RenderEmoji match={id} /> {user_ids?.size || 0}
</Reaction> </Reaction>
); );
// Desktop: show tooltip on hover
// Mobile: long-press opens modal, handled by touch events
if (!isTouchscreenDevice && userIds.length > 0) {
return (
<Tooltip content={tooltipContent} delay={300} interactive>
{reactionElement}
</Tooltip>
);
}
return reactionElement;
}), }),
[], [],
); );

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";
/** /**
@@ -28,7 +27,6 @@ type Transition =
| "SUCCESS" | "SUCCESS"
| "DISCONNECT" | "DISCONNECT"
| "RETRY" | "RETRY"
| "RETRY_FAILED"
| "LOGOUT" | "LOGOUT"
| "ONLINE" | "ONLINE"
| "OFFLINE"; | "OFFLINE";
@@ -41,7 +39,6 @@ export default class Session {
state: State = window.navigator.onLine ? "Ready" : "Offline"; state: State = window.navigator.onLine ? "Ready" : "Offline";
user_id: string | null = null; user_id: string | null = null;
client: Client | null = null; client: Client | null = null;
retryAttempts: number = 0;
/** /**
* Create a new Session * Create a new Session
@@ -91,18 +88,15 @@ export default class Session {
* Called when the client signals it has disconnected * Called when the client signals it has disconnected
*/ */
private onDropped() { private onDropped() {
if (this.state === "Connecting") { this.emit({
this.emit({ action: "RETRY_FAILED" }); action: "DISCONNECT",
} else { });
this.emit({ action: "DISCONNECT" });
}
} }
/** /**
* 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",
}); });
@@ -215,7 +209,6 @@ export default class Session {
// Ready successfully received // Ready successfully received
case "SUCCESS": { case "SUCCESS": {
this.assert("Connecting"); this.assert("Connecting");
this.retryAttempts = 0;
this.state = "Online"; this.state = "Online";
break; break;
} }
@@ -244,18 +237,6 @@ export default class Session {
this.state = "Connecting"; this.state = "Connecting";
break; break;
} }
// Reconnect attempt failed, schedule another with backoff
case "RETRY_FAILED": {
this.assert("Connecting");
this.retryAttempts++;
const delay = Math.min(500 * Math.pow(2, this.retryAttempts), 16000);
setTimeout(() => {
if (this.state === "Connecting") {
this.client!.websocket.connect();
}
}, delay);
break;
}
// User instructed logout // User instructed logout
case "LOGOUT": { case "LOGOUT": {
this.assert("Connecting", "Online", "Disconnected"); this.assert("Connecting", "Online", "Disconnected");

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";
@@ -32,8 +31,6 @@ import CreateRole from "./components/CreateRole";
import CreateServer from "./components/CreateServer"; import CreateServer from "./components/CreateServer";
import CustomStatus from "./components/CustomStatus"; import CustomStatus from "./components/CustomStatus";
import DeleteMessage from "./components/DeleteMessage"; import DeleteMessage from "./components/DeleteMessage";
import ReactMessage from "./components/ReactMessage";
import ReactionUsers from "./components/ReactionUsers";
import Error from "./components/Error"; import Error from "./components/Error";
import ImageViewer from "./components/ImageViewer"; import ImageViewer from "./components/ImageViewer";
import KickMember from "./components/KickMember"; import KickMember from "./components/KickMember";
@@ -42,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";
@@ -210,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);
@@ -277,8 +258,6 @@ export const modalController = new ModalControllerExtended({
create_bot: CreateBot, create_bot: CreateBot,
custom_status: CustomStatus, custom_status: CustomStatus,
delete_message: DeleteMessage, delete_message: DeleteMessage,
react_message: ReactMessage,
reaction_users: ReactionUsers,
error: Error, error: Error,
image_viewer: ImageViewer, image_viewer: ImageViewer,
kick_member: KickMember, kick_member: KickMember,
@@ -297,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,46 +0,0 @@
import styled from "styled-components";
import { Modal } from "@revoltchat/ui";
import { ModalProps } from "../types"
import { Message } from "revolt.js";
import { emojiDictionary } from "../../../assets/emojis"
import { HackAlertThisFileWillBeReplaced } from "../../../components/common/messaging/MessageBox"
const PickerContainer = styled.div`
max-height: 420px;
max-width: 370px;
overflow: hidden;
> div {
position: unset;
}
`
export default function ReactMessage({
target: message,
onClose,
...props
}: ModalProps<"react_message">) {
return (
<Modal
{...props}
padding={false}
maxWidth="370px"
>
<PickerContainer>
<HackAlertThisFileWillBeReplaced
onSelect={(emoji) =>{
message.react(
emojiDictionary[
emoji as keyof typeof emojiDictionary
] ?? emoji,
);
onClose();
}}
onClose={onClose}
/>
</PickerContainer>
</Modal>
)
}

View File

@@ -1,92 +0,0 @@
import styled from "styled-components";
import { Modal } from "@revoltchat/ui";
import UserIcon from "../../../components/common/user/UserIcon";
import { Username } from "../../../components/common/user/UserShort";
import { RenderEmoji } from "../../../components/markdown/plugins/emoji";
import { useClient } from "../../client/ClientController";
import { modalController } from "../ModalController";
import { ModalProps } from "../types";
const List = styled.div`
max-width: 100%;
max-height: 360px;
overflow-y: auto;
`;
const UserRow = styled.div`
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
cursor: pointer;
border-radius: var(--border-radius);
&:hover {
background: var(--secondary-background);
}
`;
const Header = styled.div`
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1em;
img {
width: 1.5em;
height: 1.5em;
object-fit: contain;
}
`;
export default function ReactionUsers({
emoji,
userIds,
onClose,
...props
}: ModalProps<"reaction_users">) {
const client = useClient();
const openProfile = (userId: string) => {
onClose();
modalController.push({ type: "user_profile", user_id: userId });
};
// Modal must be nonDismissable, it conflicts with the message context menu otherwise.
return (
<Modal
{...props}
nonDismissable
title={
<Header>
<RenderEmoji match={emoji} />
<span>{userIds.length}</span>
</Header>
}
actions={[
{
onClick: () => {
onClose();
return true;
},
children: "Close",
},
]}>
<List>
{userIds.map((userId) => {
const user = client.users.get(userId);
return (
<UserRow
key={userId}
onClick={() => openProfile(userId)}>
<UserIcon target={user} size={32} />
<Username user={user} />
</UserRow>
);
})}
</List>
</Modal>
);
}

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;
@@ -153,15 +151,6 @@ export type Modal = {
type: "delete_message"; type: "delete_message";
target: Message; target: Message;
} }
| {
type: "react_message",
target: Message;
}
| {
type: "reaction_users";
emoji: string;
userIds: string[];
}
| { | {
type: "kick_member"; type: "kick_member";
member: Member; member: Member;
@@ -190,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 }
@@ -65,7 +64,6 @@ type Action =
| { action: "quote_message"; content: string } | { action: "quote_message"; content: string }
| { action: "edit_message"; id: string } | { action: "edit_message"; id: string }
| { action: "delete_message"; target: Message } | { action: "delete_message"; target: Message }
| { action: "react_message"; target: Message }
| { action: "open_file"; attachment: API.File } | { action: "open_file"; attachment: API.File }
| { action: "save_file"; attachment: API.File } | { action: "save_file"; attachment: API.File }
| { action: "copy_file_link"; attachment: API.File } | { action: "copy_file_link"; attachment: API.File }
@@ -113,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.
@@ -134,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}`;
@@ -289,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",
); );
} }
@@ -403,13 +392,6 @@ export default function ContextMenus() {
}); });
break; break;
case "react_message":
modalController.push({
type: "react_message",
target: data.target,
});
break;
case "leave_group": case "leave_group":
case "close_dm": case "close_dm":
case "delete_channel": case "delete_channel":
@@ -467,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({
@@ -496,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;
@@ -512,19 +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" }`}
) : locale === "react_message" ? ( />
"React"
) : (
<Text
id={`app.context_menu.${
locale ?? action.action
}`}
/>
)}
</span> </span>
{tip && <div className="tip">{tip}</div>} {tip && <div className="tip">{tip}</div>}
</MenuItem>, </MenuItem>,
@@ -703,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) {
@@ -843,43 +795,14 @@ export default function ContextMenus() {
}); });
} }
if (message.channel?.havePermission("React")) {
generateAction(
{
action: "react_message",
target: message,
},
"react_message",
);
}
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 (
@@ -1112,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",
@@ -1141,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
@@ -1207,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} />

Some files were not shown because too many files have changed in this diff Show More