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"
) : (