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.
This commit is contained in:
Mariano Uvalle
2026-02-04 16:29:07 -08:00
committed by jmug
parent 41f47a1a3f
commit 1ce522579e
10 changed files with 189 additions and 11 deletions

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(
{