mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement robust reconnection system, updates, optimize typing
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @fileoverview Update Manager for Telegram-like update system
|
||||
* @description Handles update sequence numbers, batching, and gap detection
|
||||
* @author Cursor
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { openDB, type IDBPDatabase } from "idb";
|
||||
import type { WebSocketCredentials, WebSocketMessage } from "./types";
|
||||
|
||||
interface UpdateMessage<T = any> {
|
||||
type: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface BatchedUpdatesMessage {
|
||||
type: "updates";
|
||||
seq: number;
|
||||
updates: UpdateMessage[];
|
||||
}
|
||||
|
||||
const DB_NAME = "fromchat-updates";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "lastSequence";
|
||||
|
||||
let db: IDBPDatabase | null = null;
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB for storing last sequence number
|
||||
*/
|
||||
async function initDB(): Promise<IDBPDatabase> {
|
||||
if (db) return db;
|
||||
|
||||
db = await openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(database) {
|
||||
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
||||
database.createObjectStore(STORE_NAME);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last received sequence number from IndexedDB
|
||||
*/
|
||||
export async function getLastSequence(): Promise<number> {
|
||||
try {
|
||||
return (await initDB())
|
||||
.transaction(STORE_NAME, "readonly")
|
||||
.objectStore(STORE_NAME)
|
||||
.get("lastSeq") || 0;
|
||||
} catch (error) {
|
||||
console.error("Failed to get last sequence:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the last received sequence number in IndexedDB
|
||||
*/
|
||||
export async function setLastSequence(seq: number): Promise<void> {
|
||||
try {
|
||||
(await initDB()).transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(seq, "lastSeq");
|
||||
} catch (error) {
|
||||
console.error("Failed to set last sequence:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batched updates message
|
||||
* @param message - The batched updates message from the server
|
||||
* @param handler - Function to handle individual updates
|
||||
* @param requestMissedFn - Optional function to request missed updates (for gap detection)
|
||||
*/
|
||||
export async function processBatchedUpdates(
|
||||
message: BatchedUpdatesMessage,
|
||||
handler: (update: UpdateMessage) => void,
|
||||
requestMissedFn?: (lastSeq: number) => Promise<void>
|
||||
): Promise<void> {
|
||||
const { seq, updates } = message;
|
||||
const lastSeq = await getLastSequence();
|
||||
|
||||
// Check for gap
|
||||
if (seq !== lastSeq + 1 && lastSeq > 0) {
|
||||
console.warn(`Update gap detected: expected ${lastSeq + 1}, got ${seq}`);
|
||||
|
||||
// Request missing updates if function provided
|
||||
if (requestMissedFn) {
|
||||
try {
|
||||
await requestMissedFn(lastSeq);
|
||||
} catch (error) {
|
||||
console.error("Failed to request missed updates for gap:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process all updates in the batch
|
||||
for (const update of updates) {
|
||||
handler(update);
|
||||
}
|
||||
|
||||
// Update last sequence number
|
||||
await setLastSequence(seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request missed updates from the server
|
||||
* @param lastSeq - The last sequence number we received
|
||||
* @param requestFn - Function to send the request to the server
|
||||
* @param credentials - Optional WebSocket credentials for authentication
|
||||
*/
|
||||
export async function requestMissedUpdates(
|
||||
lastSeq: number,
|
||||
requestFn: (request: WebSocketMessage<{ lastSeq: number }>) => Promise<void>,
|
||||
credentials?: WebSocketCredentials
|
||||
): Promise<void> {
|
||||
if (lastSeq > 0) {
|
||||
await requestFn({
|
||||
type: "getUpdates",
|
||||
data: { lastSeq },
|
||||
credentials
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { CallSignalingHandler } from "./calls/signaling";
|
||||
import { onlineStatusManager } from "./onlineStatusManager";
|
||||
import { typingManager } from "./typingManager";
|
||||
import { useUserStore } from "@/state/user";
|
||||
import { getLastSequence, processBatchedUpdates, requestMissedUpdates } from "./updateManager";
|
||||
import { getAuthToken } from "@/core/api/user/auth";
|
||||
|
||||
/**
|
||||
* Creates a new WebSocket connection to the chat server
|
||||
@@ -148,55 +150,119 @@ async function reconnect(): Promise<void> {
|
||||
*/
|
||||
function setupEventHandlers(): void {
|
||||
// Message handler
|
||||
messageHandler = (e: MessageEvent) => {
|
||||
messageHandler = async (e: MessageEvent) => {
|
||||
try {
|
||||
const response: WebSocketMessage<any> = JSON.parse(e.data);
|
||||
|
||||
// Handle batched updates
|
||||
if (response.type === "updates" && "seq" in response && "updates" in response) {
|
||||
// Create function to request missed updates with credentials
|
||||
const token = getAuthToken();
|
||||
const requestMissedFn = token ? async (lastSeq: number) => {
|
||||
await requestMissedUpdates(lastSeq, async (req) => {
|
||||
await request(req);
|
||||
}, {
|
||||
scheme: "Bearer",
|
||||
credentials: token
|
||||
});
|
||||
} : undefined;
|
||||
|
||||
await processBatchedUpdates(response as any, (update) => {
|
||||
// Route individual updates to appropriate handlers
|
||||
handleUpdate(update);
|
||||
}, requestMissedFn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle call signaling messages
|
||||
if (callSignalingHandler && response.type === "call_signaling" && response.data) {
|
||||
callSignalingHandler.handleWebSocketMessage(response.data);
|
||||
}
|
||||
|
||||
// Handle status and typing messages
|
||||
if (response.type === "statusUpdate") {
|
||||
onlineStatusManager.handleStatusUpdate(response as any);
|
||||
} else if (response.type === "typing") {
|
||||
typingManager.handleTyping(response as any);
|
||||
} else if (response.type === "stopTyping") {
|
||||
typingManager.handleStopTyping(response as any);
|
||||
} else if (response.type === "dmTyping") {
|
||||
typingManager.handleDmTyping(response as any);
|
||||
} else if (response.type === "stopDmTyping") {
|
||||
typingManager.handleStopDmTyping(response as any);
|
||||
} else if (response.type === "suspended") {
|
||||
// Handle account suspension
|
||||
const { setSuspended } = useUserStore.getState();
|
||||
const reason = response.data?.reason || "No reason provided";
|
||||
setSuspended(reason);
|
||||
// Close WebSocket connection
|
||||
websocket.close();
|
||||
} else if (response.type === "account_deleted") {
|
||||
// Handle account deletion - silent logout
|
||||
const { logout } = useUserStore.getState();
|
||||
logout();
|
||||
// Close WebSocket connection
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
// Route message to global handler if set
|
||||
if (globalMessageHandler) {
|
||||
globalMessageHandler(response);
|
||||
}
|
||||
// Handle status and typing messages (these may come as immediate messages or in batches)
|
||||
handleUpdate(response);
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle individual updates
|
||||
function handleUpdate(response: WebSocketMessage<any>): void {
|
||||
if (response.type === "statusUpdate") {
|
||||
onlineStatusManager.handleStatusUpdate(response as any);
|
||||
} else if (response.type === "typing") {
|
||||
typingManager.handleTyping(response as any);
|
||||
} else if (response.type === "stopTyping") {
|
||||
typingManager.handleStopTyping(response as any);
|
||||
} else if (response.type === "dmTyping") {
|
||||
typingManager.handleDmTyping(response as any);
|
||||
} else if (response.type === "stopDmTyping") {
|
||||
typingManager.handleStopDmTyping(response as any);
|
||||
} else if (response.type === "suspended") {
|
||||
// Handle account suspension
|
||||
const { setSuspended } = useUserStore.getState();
|
||||
const reason = response.data?.reason || "No reason provided";
|
||||
setSuspended(reason);
|
||||
// Close WebSocket connection
|
||||
websocket.close();
|
||||
} else if (response.type === "account_deleted") {
|
||||
// Handle account deletion - silent logout
|
||||
const { logout } = useUserStore.getState();
|
||||
logout();
|
||||
// Close WebSocket connection
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
// Route message to global handler if set
|
||||
if (globalMessageHandler) {
|
||||
globalMessageHandler(response);
|
||||
}
|
||||
}
|
||||
websocket.addEventListener("message", messageHandler);
|
||||
|
||||
// Open handler
|
||||
openHandler = () => {
|
||||
openHandler = async () => {
|
||||
reconnectAttempts = 0; // Reset on successful connection
|
||||
isReconnecting = false;
|
||||
|
||||
// Authenticate by sending ping with credentials and request missed updates
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
const credentials = {
|
||||
scheme: "Bearer",
|
||||
credentials: token
|
||||
};
|
||||
|
||||
// Send ping to authenticate and set user_by_ws on the server
|
||||
try {
|
||||
await request({
|
||||
type: "ping",
|
||||
credentials,
|
||||
data: {}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send ping on reconnect:", error);
|
||||
}
|
||||
|
||||
// Send last sequence number and request missed updates on reconnect
|
||||
// Wait a bit for ping to complete authentication
|
||||
await delay(100);
|
||||
|
||||
try {
|
||||
const lastSeq = await getLastSequence();
|
||||
if (lastSeq > 0) {
|
||||
await requestMissedUpdates(lastSeq, async (req) => {
|
||||
await request(req);
|
||||
}, credentials);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to request missed updates:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to authenticate on reconnect:", error);
|
||||
}
|
||||
};
|
||||
websocket.addEventListener("open", openHandler);
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
||||
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const previousMessageCountRef = useRef(0);
|
||||
const messagesContainerRef = useRef<HTMLElement | null>(null);
|
||||
const isLoadingMoreRef = useRef(false);
|
||||
const [replyTo, setReplyTo] = useState<Message | null>(null);
|
||||
const [replyToVisible, setReplyToVisible] = useState(Boolean(replyTo));
|
||||
const [editMessage, setEditMessage] = useState<Message | null>(null);
|
||||
@@ -92,6 +94,50 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
||||
}
|
||||
}, [editMessage]);
|
||||
|
||||
// Handle scroll detection for infinite loading
|
||||
useEffect(() => {
|
||||
if (!panel || !panelState) return;
|
||||
|
||||
const messagesContainer = document.getElementById("chat-messages");
|
||||
if (!messagesContainer) return;
|
||||
|
||||
messagesContainerRef.current = messagesContainer;
|
||||
|
||||
const handleScroll = async () => {
|
||||
if (!panel || !panelState || isLoadingMoreRef.current) return;
|
||||
|
||||
const container = messagesContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Check if scrolled to top (within 100px threshold)
|
||||
if (container.scrollTop <= 100 && panelState.hasMoreMessages && !panelState.isLoadingMore) {
|
||||
isLoadingMoreRef.current = true;
|
||||
const previousScrollHeight = container.scrollHeight;
|
||||
|
||||
try {
|
||||
await panel.loadMoreMessages();
|
||||
|
||||
// Preserve scroll position after loading
|
||||
requestAnimationFrame(() => {
|
||||
if (container) {
|
||||
const newScrollHeight = container.scrollHeight;
|
||||
container.scrollTop = newScrollHeight - previousScrollHeight;
|
||||
}
|
||||
isLoadingMoreRef.current = false;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading more messages:", error);
|
||||
isLoadingMoreRef.current = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
messagesContainer.addEventListener("scroll", handleScroll);
|
||||
return () => {
|
||||
messagesContainer.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [panel, panelState]);
|
||||
|
||||
// Handle panel state changes
|
||||
useEffect(() => {
|
||||
if (panel) {
|
||||
@@ -280,31 +326,43 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : panelState && panel ? (
|
||||
<ChatMessages
|
||||
messages={panelState.messages}
|
||||
isDm={panel.isDm()}
|
||||
dmRecipientPublicKey={(panel as DMPanel).dmData?.publicKey}
|
||||
onReplySelect={(message) => {
|
||||
if (editMessage || editVisible) {
|
||||
setPendingAction({ type: "reply", message: message });
|
||||
setEditVisible(false); // onCloseEdit will apply pending
|
||||
} else {
|
||||
setReplyTo(message);
|
||||
}
|
||||
}}
|
||||
onEditSelect={(message) => {
|
||||
if (replyTo || replyToVisible) {
|
||||
setPendingAction({ type: "edit", message: message });
|
||||
setReplyToVisible(false); // onCloseReply will apply pending
|
||||
} else {
|
||||
setEditMessage(message);
|
||||
}
|
||||
}}
|
||||
onDelete={(id) => panel.handleDeleteMessage(id)}
|
||||
onRetryMessage={(id) => panel.retryMessage(id)}
|
||||
>
|
||||
<div ref={messagesEndRef} />
|
||||
</ChatMessages>
|
||||
<>
|
||||
{panelState.isLoadingMore && (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
padding: "8px",
|
||||
color: "var(--mdui-color-on-surface-variant)"
|
||||
}}>
|
||||
Загрузка...
|
||||
</div>
|
||||
)}
|
||||
<ChatMessages
|
||||
messages={panelState.messages}
|
||||
isDm={panel.isDm()}
|
||||
dmRecipientPublicKey={(panel as DMPanel).dmData?.publicKey}
|
||||
onReplySelect={(message) => {
|
||||
if (editMessage || editVisible) {
|
||||
setPendingAction({ type: "reply", message: message });
|
||||
setEditVisible(false); // onCloseEdit will apply pending
|
||||
} else {
|
||||
setReplyTo(message);
|
||||
}
|
||||
}}
|
||||
onEditSelect={(message) => {
|
||||
if (replyTo || replyToVisible) {
|
||||
setPendingAction({ type: "edit", message: message });
|
||||
setReplyToVisible(false); // onCloseReply will apply pending
|
||||
} else {
|
||||
setEditMessage(message);
|
||||
}
|
||||
}}
|
||||
onDelete={(id) => panel.handleDeleteMessage(id)}
|
||||
onRetryMessage={(id) => panel.retryMessage(id)}
|
||||
>
|
||||
<div ref={messagesEndRef} />
|
||||
</ChatMessages>
|
||||
</>
|
||||
) : (
|
||||
<div className={rightPanelStyles.chatMessages} id="chat-messages">
|
||||
<div style={{
|
||||
|
||||
@@ -103,7 +103,8 @@ export class DMPanel extends MessagePanel {
|
||||
|
||||
this.setLoading(true);
|
||||
try {
|
||||
const { messages } = await api.chats.dm.fetchMessages(this.dmData.userId, this.currentUser.authToken, 50);
|
||||
const limit = this.calculateMessageLimit();
|
||||
const { messages, has_more } = await api.chats.dm.fetchMessages(this.dmData.userId, this.currentUser.authToken, limit);
|
||||
const decryptedMessages: Message[] = [];
|
||||
let maxIncomingId = 0;
|
||||
|
||||
@@ -122,6 +123,7 @@ export class DMPanel extends MessagePanel {
|
||||
|
||||
this.clearMessages();
|
||||
decryptedMessages.forEach(msg => this.addMessage(msg));
|
||||
this.setHasMoreMessages(has_more);
|
||||
|
||||
// Update last read ID
|
||||
if (maxIncomingId > 0) {
|
||||
@@ -135,6 +137,50 @@ export class DMPanel extends MessagePanel {
|
||||
}
|
||||
}
|
||||
|
||||
async loadMoreMessages(): Promise<void> {
|
||||
if (!this.currentUser.authToken || !this.dmData || !this.state.hasMoreMessages || this.state.isLoadingMore) return;
|
||||
|
||||
const messages = this.getMessages();
|
||||
if (messages.length === 0) return;
|
||||
|
||||
const oldestMessage = messages[0];
|
||||
const oldestEnvelope = oldestMessage.runtimeData?.dmEnvelope;
|
||||
if (!oldestEnvelope) return;
|
||||
|
||||
this.setLoadingMore(true);
|
||||
try {
|
||||
const limit = this.calculateMessageLimit();
|
||||
const { messages: newEnvelopes, has_more } = await api.chats.dm.fetchMessages(
|
||||
this.dmData.userId,
|
||||
this.currentUser.authToken,
|
||||
limit,
|
||||
oldestEnvelope.id
|
||||
);
|
||||
|
||||
if (newEnvelopes && newEnvelopes.length > 0) {
|
||||
const decryptedMessages: Message[] = [];
|
||||
for (const env of newEnvelopes) {
|
||||
try {
|
||||
const dmMsg = await this.parseTextPayload(env, decryptedMessages);
|
||||
decryptedMessages.push(dmMsg);
|
||||
} catch (error) {
|
||||
console.error("Error decrypting message:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend older messages (they come in reverse chronological order)
|
||||
this.updateState({
|
||||
messages: [...decryptedMessages.reverse(), ...messages]
|
||||
});
|
||||
}
|
||||
this.setHasMoreMessages(has_more);
|
||||
} catch (error) {
|
||||
console.error("Failed to load more DM messages:", error);
|
||||
} finally {
|
||||
this.setLoadingMore(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected async sendMessage(content: string, replyToId?: number, files: File[] = []): Promise<void> {
|
||||
if (!this.currentUser.authToken || !this.dmData || !content.trim()) return;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface MessagePanelState {
|
||||
messages: Message[];
|
||||
isLoading: boolean;
|
||||
isTyping: boolean;
|
||||
hasMoreMessages: boolean;
|
||||
isLoadingMore: boolean;
|
||||
}
|
||||
|
||||
export interface MessagePanelCallbacks {
|
||||
@@ -35,7 +37,9 @@ export abstract class MessagePanel {
|
||||
online: false,
|
||||
messages: [],
|
||||
isLoading: false,
|
||||
isTyping: false
|
||||
isTyping: false,
|
||||
hasMoreMessages: false,
|
||||
isLoadingMore: false
|
||||
};
|
||||
this.currentUser = currentUser;
|
||||
}
|
||||
@@ -107,6 +111,27 @@ export abstract class MessagePanel {
|
||||
this.updateState({ isTyping: typing });
|
||||
}
|
||||
|
||||
protected setLoadingMore(loading: boolean): void {
|
||||
this.updateState({ isLoadingMore: loading });
|
||||
}
|
||||
|
||||
protected setHasMoreMessages(hasMore: boolean): void {
|
||||
this.updateState({ hasMoreMessages: hasMore });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate message limit based on viewport height (5x screen height)
|
||||
*/
|
||||
protected calculateMessageLimit(): number {
|
||||
const viewportHeight = window.innerHeight;
|
||||
return Math.ceil((viewportHeight * 5) / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more messages (to be implemented by subclasses)
|
||||
*/
|
||||
abstract loadMoreMessages(): Promise<void>;
|
||||
|
||||
// Getters
|
||||
getState(): MessagePanelState {
|
||||
return { ...this.state };
|
||||
|
||||
@@ -41,13 +41,15 @@ export class PublicChatPanel extends MessagePanel {
|
||||
|
||||
this.setLoading(true);
|
||||
try {
|
||||
const { messages } = await api.chats.general.fetchMessages(this.currentUser.authToken);
|
||||
const limit = this.calculateMessageLimit();
|
||||
const { messages, has_more } = await api.chats.general.fetchMessages(this.currentUser.authToken, limit);
|
||||
if (messages && messages.length > 0) {
|
||||
this.clearMessages();
|
||||
messages.forEach((msg: Message) => {
|
||||
this.addMessage(msg);
|
||||
});
|
||||
}
|
||||
this.setHasMoreMessages(has_more);
|
||||
this.messagesLoaded = true;
|
||||
} catch (error) {
|
||||
console.error("Error loading public chat messages:", error);
|
||||
@@ -56,6 +58,35 @@ export class PublicChatPanel extends MessagePanel {
|
||||
}
|
||||
}
|
||||
|
||||
async loadMoreMessages(): Promise<void> {
|
||||
if (!this.currentUser.authToken || !this.state.hasMoreMessages || this.state.isLoadingMore) return;
|
||||
|
||||
const messages = this.getMessages();
|
||||
if (messages.length === 0) return;
|
||||
|
||||
const oldestMessage = messages[0];
|
||||
this.setLoadingMore(true);
|
||||
try {
|
||||
const limit = this.calculateMessageLimit();
|
||||
const { messages: newMessages, has_more } = await api.chats.general.fetchMessages(
|
||||
this.currentUser.authToken,
|
||||
limit,
|
||||
oldestMessage.id
|
||||
);
|
||||
if (newMessages && newMessages.length > 0) {
|
||||
// Prepend older messages (they come in reverse chronological order)
|
||||
this.updateState({
|
||||
messages: [...newMessages.reverse(), ...messages]
|
||||
});
|
||||
}
|
||||
this.setHasMoreMessages(has_more);
|
||||
} catch (error) {
|
||||
console.error("Error loading more public chat messages:", error);
|
||||
} finally {
|
||||
this.setLoadingMore(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected async sendMessage(content: string, replyToId?: number, files: File[] = []): Promise<void> {
|
||||
if (!this.currentUser.authToken || !content.trim()) return;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import type { User } from "@/core/types";
|
||||
import { request } from "@/core/websocket";
|
||||
import api from "@/core/api";
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
||||
@@ -44,16 +43,8 @@ export const useUserStore = create<UserStore>((set) => ({
|
||||
console.error('Failed to store credentials in localStorage:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
request({
|
||||
type: "ping",
|
||||
credentials: {
|
||||
scheme: "Bearer",
|
||||
credentials: token
|
||||
},
|
||||
data: {}
|
||||
})
|
||||
} catch {}
|
||||
// Ping will be sent automatically on WebSocket reconnect
|
||||
// No need to send here to avoid duplicate pings
|
||||
},
|
||||
logout: () => {
|
||||
try {
|
||||
@@ -113,16 +104,8 @@ export const useUserStore = create<UserStore>((set) => ({
|
||||
onlineStatusManager.setAuthToken(token);
|
||||
typingManager.setAuthToken(token);
|
||||
|
||||
try {
|
||||
request({
|
||||
type: "ping",
|
||||
credentials: {
|
||||
scheme: "Bearer",
|
||||
credentials: token
|
||||
},
|
||||
data: {}
|
||||
})
|
||||
} catch {}
|
||||
// Ping will be sent automatically on WebSocket reconnect
|
||||
// No need to send here to avoid duplicate pings
|
||||
|
||||
try {
|
||||
if (isSupported()) {
|
||||
|
||||
Reference in New Issue
Block a user