From 1ce522579e881d61adc7835d29c46a79165a50b7 Mon Sep 17 00:00:00 2001 From: Mariano Uvalle Date: Wed, 4 Feb 2026 16:29:07 -0800 Subject: [PATCH] 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. --- .envrc | 1 + .github/workflows/docker.yml | 10 +-- .gitignore | 2 + flake.lock | 64 +++++++++++++++++++ flake.nix | 28 ++++++++ src/controllers/client/Session.tsx | 23 ++++++- src/controllers/modals/ModalController.tsx | 2 + .../modals/components/ReactMessage.tsx | 46 +++++++++++++ src/controllers/modals/types.ts | 4 ++ src/lib/ContextMenus.tsx | 20 ++++++ 10 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/controllers/modals/components/ReactMessage.tsx diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f7732836..590b7b52 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -46,22 +46,16 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: revoltchat/client, ghcr.io/revoltchat/client + images: ghcr.io/AYM1607/revoltchat-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 }} + password: ${{ secrets.GCR_TOKEN }} - name: Build and publish uses: docker/build-push-action@v6 with: diff --git a/.gitignore b/.gitignore index 5439381b..3a1be93e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ public/assets_* !public/assets_default .vscode/chrome_data + +.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..279cd6e8 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..5ef1db8b --- /dev/null +++ b/flake.nix @@ -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 + ]; + }; + } + ); +} diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index 38820823..1e79fe53 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -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"); diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index ebed19c6..676795f8 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -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, diff --git a/src/controllers/modals/components/ReactMessage.tsx b/src/controllers/modals/components/ReactMessage.tsx new file mode 100644 index 00000000..95ff4f98 --- /dev/null +++ b/src/controllers/modals/components/ReactMessage.tsx @@ -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 ( + + + { + message.react( + emojiDictionary[ + emoji as keyof typeof emojiDictionary + ] ?? emoji, + ); + onClose(); + }} + onClose={onClose} + /> + + + ) +} diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index 9255d173..ff46fb69 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -153,6 +153,10 @@ export type Modal = { type: "delete_message"; target: Message; } + | { + type: "react_message", + target: Message; + } | { type: "kick_member"; member: Member; diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 0999b38d..66c78141 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -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" ) : (