Compare commits

...

33 Commits

Author SHA1 Message Date
jmug 1b2a980a47 [chore] Workflow cleanup.
Docker / publish (push) Successful in 13m59s Details
2026-02-23 01:55:29 -08:00
Mariano Uvalle 1ce522579e Mobile reactions and WS reconnection
* [feat] Add reaction menu item for mobile clients.

* [fix] Reconnection to the websocket with exponential backoff.

* [chore] Dev flake.

* [hack] Push to my personal gcr.
2026-02-22 13:47:39 -08:00
Paul Makles 41f47a1a3f
merge: pull request #1143 from Asraye/fix/member-sidebar 2025-09-07 13:09:46 +01:00
Paul Makles c6130a19bf
merge: pull request #1109 from Docteh/smaller3 2025-09-07 13:06:39 +01:00
Paul Makles 3c7cb2a222
merge: pull request #1141 from Asraye/fixemoji-pack 2025-09-07 13:05:41 +01:00
Paul Makles 2625b37f70
merge: pull request #1142 from Asraye/ignore-default-category 2025-09-07 13:04:47 +01:00
Paul Makles ca3613725a
merge: pull request #1137 from Asraye/master 2025-09-07 13:04:05 +01:00
Paul Makles 707d0a2d7d
merge: pull request #1146 from Asraye/fix/attachment-actions 2025-09-07 13:01:20 +01:00
Asraye 33e686ab1b
fix: correct open/download link behaviour 2025-09-04 04:23:09 +10:00
Asraye f22e6fa9fe
fix: correct open/download link behaviour 2025-09-04 04:22:20 +10:00
Asraye 2e2886f01b
fix: filter out members without ViewChannel
Small fix to add a ViewChannel check when rendering members in the sidebar
2025-09-02 16:14:10 +10:00
Asraye cbdeb1ba6d FIX: Ignore Default category (parity with beta)
Just a little change so people still using Revite don't need to see an ugly "Default" category.
2025-08-31 03:47:43 +10:00
Asraye 608b0a3ef2
FIX: Emoji pack now persists on reload 2025-08-29 21:55:56 +10:00
Asraye 59226959ba
Add scroll on mobile 2025-08-21 16:40:51 +10:00
izzy 9d82873d2a chore: make sure role ordering updates are instantenous 2025-08-07 16:57:20 +02:00
izzy 56af4b9423 chore: add default.nix 2025-08-07 12:04:45 +02:00
izzy cce94d2e0e chore: bump lang submodule 2025-08-07 12:03:54 +02:00
izzy 8ba7a769b3 merge: remote-tracking branch 'origin/role-reordering' 2025-08-07 12:03:35 +02:00
Paul Makles 92e84309ec
merge: pull request #1123 from Asraye/patch 2025-08-04 18:03:53 +01:00
Paul Makles dd5548c5e6
merge: pull request #1126 from solunareclipse1/master 2025-08-04 17:44:49 +01:00
Declan Chidlow e7d7420d5b feat: Finish crappy role ranking implementation that works but is full of sin 2025-08-04 22:28:24 +08:00
izzy ee3b54b373
docs: shorten age gate message 2025-07-25 20:34:45 +01:00
izzy 3dc9f6b045
feat: geoblock age restricted content
https://www.ofcom.org.uk/online-safety/protecting-children/online-age-checks-must-be-in-force-from-tomorrow
https://petition.parliament.uk/petitions/722903
https://wikimediafoundation.org/news/2025/07/17/wikimedia-foundation-challenges-uk-online-safety-act-regulations/
2025-07-25 19:49:00 +01:00
izzy 2a1f7cb8bb
chore: bump lang submodule 2025-07-25 19:48:16 +01:00
solunareclipse1 1136e8e31b
fix: Make animated WebP emotes properly animate
Signed-off-by: solunareclipse1 <sol@solunareclipse1.net>
2025-07-10 23:41:57 -05:00
Asraye 5337031359
Update EmbedInvite.tsx 2025-06-12 16:26:09 +10:00
Asraye 670c4e5f6e
Update EmbedInvite.tsx 2025-06-12 15:46:07 +10:00
Declan Chidlow 20c8dde197 feat: Add role re-ordering
Doesn't currently update order in UI
2025-06-12 12:33:39 +08:00
Declan Chidlow ddaca7b0c4 feat: remove legacy role ranking 2025-06-12 09:59:57 +08:00
Asraye 94fab0852f
Ugly ahh patch 2025-06-11 18:08:33 +10:00
Kyle Kienapfel 2c41c5789c feat: drastically shrink the docker image
Right now the generated docker container contains a copy of the development environment used to compile the revite interface.
This PR loses about 440MB compressed, or around 800MB uncompressed from the final image size. node in the final image has been bumped from 16 to 24, sirv-cli, and the dependencies for inject.js have been updated
2025-05-18 01:59:51 -07:00
Kyle Kienapfel 184754ca0f chore: have GHA generate ARM64 image, add description to images
The box where githubs container website complains about a lack
a description is really big. Okay its not important.
2025-05-18 01:29:54 -07:00
Kyle Kienapfel 6bacbfbb56 fix: move disabled-js.svg to proper location 2025-05-18 01:29:17 -07:00
28 changed files with 891 additions and 413 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

View File

@ -3,36 +3,38 @@ name: Docker
on:
push:
branches:
- "master"
- "handmade"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
# TODO: Bring back once gitea is updated past 1.21
# paths-ignore:
# - ".github/**"
# - "!.github/workflows/docker.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"
- "handmade"
# TODO: Bring back once gitea is updated past 1.21
# paths-ignore:
# - ".github/**"
# - "!.github/workflows/docker.yml"
# - "!.github/workflows/preview_*.yml"
# - ".vscode/**"
# - ".gitignore"
# - ".gitlab-ci.yml"
# - "LICENSE"
# - "README"
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
# NOTE: Running on pull requests for now, but without pushing.
# if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
@ -44,27 +46,23 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: revoltchat/client, ghcr.io/revoltchat/client
images: handmadecities/handmade-revolt-web-client
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Login to DockerHub
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,54 +0,0 @@
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"){
projectV2(number: 3) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .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!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@ -1,79 +0,0 @@
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"){
projectV2(number: 5) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.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!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.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: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: {
singleSelectOptionId: $status_value
}
}) {
projectV2Item {
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,3 +15,5 @@ public/assets_*
!public/assets_default
.vscode/chrome_data
.direnv

View File

@ -1,4 +1,5 @@
FROM node:16-buster AS builder
# syntax=docker.io/docker/dockerfile:1.7-labs
FROM --platform=$BUILDPLATFORM node:16-buster AS builder
WORKDIR /usr/src/app
COPY . .
@ -10,9 +11,11 @@ RUN yarn build:deps
RUN yarn build:highmem
RUN yarn workspaces focus --production --all
FROM node:16-alpine
FROM node:24-alpine
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app .
COPY docker/package.json docker/yarn.lock .
RUN yarn install --frozen-lockfile
COPY --from=builder --exclude=package.json --exclude=yarn.lock --exclude=.yarn* --exclude=.git --exclude=external --exclude=node_modules /usr/src/app .
EXPOSE 5000
CMD [ "yarn", "start:inject" ]

8
default.nix Normal file
View File

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

16
docker/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"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"
}
}

116
docker/yarn.lock Normal file
View File

@ -0,0 +1,116 @@
# 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

@ -1 +1 @@
Subproject commit bb4bfcfd5a22dfc84a1a9f797a43af6804d96b93
Subproject commit 788261ef41e083ec917ee9bee37bc5b0e4f8d153

2
external/revolt.js vendored

@ -1 +1 @@
Subproject commit cd9e84a337c72709b82bb4eca794ec7474a0ee7e
Subproject commit a45710f80cd7b4424a2114d2a32cbb83a4d95761

64
flake.lock Normal file
View File

@ -0,0 +1,64 @@
{
"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
}

28
flake.nix Normal file
View File

@ -0,0 +1,28 @@
{
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

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 828 B

View File

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

View File

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

View File

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

View File

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

View File

@ -50,8 +50,10 @@ const ServerList = styled.div`
export default observer(() => {
const client = useClient();
const state = useApplicationState();
const { server: server_id, channel: channel_id } =
useParams<{ server: string; channel?: string }>();
const { server: server_id, channel: channel_id } = useParams<{
server: string;
channel?: string;
}>();
const server = client.servers.get(server_id);
if (!server) return <Redirect to="/" />;
@ -119,6 +121,9 @@ export default observer(() => {
channels.push(addChannel(id));
}
if (category.title === "Default") {
elements.push(...channels);
} else {
elements.push(
<CollapsibleSection
id={`category_${category.id}`}
@ -129,6 +134,7 @@ export default observer(() => {
);
}
}
}
for (const id of Array.from(uncategorised).reverse()) {
elements.unshift(addChannel(id));

View File

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

View File

@ -28,6 +28,7 @@ type Transition =
| "SUCCESS"
| "DISCONNECT"
| "RETRY"
| "RETRY_FAILED"
| "LOGOUT"
| "ONLINE"
| "OFFLINE";
@ -40,6 +41,7 @@ export default class Session {
state: State = window.navigator.onLine ? "Ready" : "Offline";
user_id: string | null = null;
client: Client | null = null;
retryAttempts: number = 0;
/**
* Create a new Session
@ -89,9 +91,11 @@ export default class Session {
* Called when the client signals it has disconnected
*/
private onDropped() {
this.emit({
action: "DISCONNECT",
});
if (this.state === "Connecting") {
this.emit({ action: "RETRY_FAILED" });
} else {
this.emit({ action: "DISCONNECT" });
}
}
/**
@ -211,6 +215,7 @@ export default class Session {
// Ready successfully received
case "SUCCESS": {
this.assert("Connecting");
this.retryAttempts = 0;
this.state = "Online";
break;
}
@ -239,6 +244,18 @@ export default class Session {
this.state = "Connecting";
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
case "LOGOUT": {
this.assert("Connecting", "Online", "Disconnected");

View File

@ -32,6 +32,7 @@ import CreateRole from "./components/CreateRole";
import CreateServer from "./components/CreateServer";
import CustomStatus from "./components/CustomStatus";
import DeleteMessage from "./components/DeleteMessage";
import ReactMessage from "./components/ReactMessage";
import Error from "./components/Error";
import ImageViewer from "./components/ImageViewer";
import KickMember from "./components/KickMember";
@ -275,6 +276,7 @@ export const modalController = new ModalControllerExtended({
create_bot: CreateBot,
custom_status: CustomStatus,
delete_message: DeleteMessage,
react_message: ReactMessage,
error: Error,
image_viewer: ImageViewer,
kick_member: KickMember,

View File

@ -0,0 +1,46 @@
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

@ -153,6 +153,10 @@ export type Modal = {
type: "delete_message";
target: Message;
}
| {
type: "react_message",
target: Message;
}
| {
type: "kick_member";
member: Member;

View File

@ -65,6 +65,7 @@ type Action =
| { action: "quote_message"; content: string }
| { action: "edit_message"; id: string }
| { action: "delete_message"; target: Message }
| { action: "react_message"; target: Message }
| { action: "open_file"; attachment: API.File }
| { action: "save_file"; attachment: API.File }
| { action: "copy_file_link"; attachment: API.File }
@ -402,6 +403,13 @@ export default function ContextMenus() {
});
break;
case "react_message":
modalController.push({
type: "react_message",
target: data.target,
});
break;
case "leave_group":
case "close_dm":
case "delete_channel":
@ -508,6 +516,8 @@ export default function ContextMenus() {
"Open in Admin Panel"
) : locale === "admin_system" ? (
"Open User in Admin Panel"
) : locale === "react_message" ? (
"React"
) : (
<Text
id={`app.context_menu.${
@ -833,6 +843,16 @@ export default function ContextMenus() {
});
}
if (message.channel?.havePermission("React")) {
generateAction(
{
action: "react_message",
target: message,
},
"react_message",
);
}
if (message.author_id !== userId) {
generateAction(
{

View File

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

View File

@ -10,6 +10,22 @@ import UserIcon from "../../components/common/user/UserIcon";
import Markdown from "../../components/markdown/Markdown";
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`
gap: 12px;
display: flex;
@ -42,7 +58,7 @@ export default function InviteBot() {
const [group, setGroup] = useState("none");
return (
<div style={{ padding: "6em" }}>
<Page>
<Tip palette="warning">This section is under construction.</Tip>
{typeof data === "undefined" && <Preloader type="spinner" />}
{data && (
@ -106,6 +122,7 @@ export default function InviteBot() {
</Option>
</>
)}
</div>
</Page>
);
}

View File

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