feat: realtime sync, settings UX and chat list improvements
Some checks failed
CI / test (push) Failing after 21s
Some checks failed
CI / test (push) Failing after 21s
- add chat_updated realtime event and dynamic chat subscriptions - auto-join invite links in web app - implement Telegram-like settings panel (general/notifications/privacy) - add browser notification preferences and keyboard send mode - improve chat list with last message preview/time and online badge - rework chat members UI with context actions and role crowns
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import type { Message } from "../chat/types";
|
||||
import { getAppPreferences } from "../utils/preferences";
|
||||
import { buildWsUrl } from "../utils/ws";
|
||||
|
||||
interface RealtimeEnvelope {
|
||||
@@ -21,6 +22,7 @@ export function useRealtime() {
|
||||
const lastPongAtRef = useRef<number>(Date.now());
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const manualCloseRef = useRef(false);
|
||||
const notificationPermissionRequestedRef = useRef(false);
|
||||
|
||||
const wsUrl = useMemo(() => {
|
||||
return accessToken ? buildWsUrl(accessToken) : null;
|
||||
@@ -67,6 +69,10 @@ export function useRealtime() {
|
||||
if (store.activeChatId) {
|
||||
void store.loadMessages(store.activeChatId);
|
||||
}
|
||||
if ("Notification" in window && Notification.permission === "default" && !notificationPermissionRequestedRef.current) {
|
||||
notificationPermissionRequestedRef.current = true;
|
||||
void Notification.requestPermission();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (messageEvent) => {
|
||||
@@ -99,11 +105,21 @@ export function useRealtime() {
|
||||
} else if (wasInserted) {
|
||||
chatStore.incrementUnread(chatId);
|
||||
}
|
||||
maybeShowBrowserNotification(chatId, message, chatStore.activeChatId);
|
||||
}
|
||||
if (!chatStore.chats.some((chat) => chat.id === chatId)) {
|
||||
void chatStore.loadChats();
|
||||
}
|
||||
}
|
||||
if (event.event === "chat_updated") {
|
||||
const chatId = Number(event.payload.chat_id);
|
||||
if (Number.isFinite(chatId)) {
|
||||
void chatStore.loadChats();
|
||||
if (chatStore.activeChatId === chatId) {
|
||||
void chatStore.loadMessages(chatId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.event === "pong") {
|
||||
lastPongAtRef.current = Date.now();
|
||||
}
|
||||
@@ -216,3 +232,49 @@ export function useRealtime() {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function maybeShowBrowserNotification(chatId: number, message: Message, activeChatId: number | null): void {
|
||||
const prefs = getAppPreferences();
|
||||
if (!prefs.webNotifications) {
|
||||
return;
|
||||
}
|
||||
if (!("Notification" in window) || Notification.permission !== "granted") {
|
||||
return;
|
||||
}
|
||||
if (!document.hidden && activeChatId === chatId) {
|
||||
return;
|
||||
}
|
||||
const chat = useChatStore.getState().chats.find((item) => item.id === chatId);
|
||||
if (chat?.type === "private" && !prefs.privateNotifications) {
|
||||
return;
|
||||
}
|
||||
if (chat?.type === "group" && !prefs.groupNotifications) {
|
||||
return;
|
||||
}
|
||||
if (chat?.type === "channel" && !prefs.channelNotifications) {
|
||||
return;
|
||||
}
|
||||
const title = chat?.display_title || chat?.title || "New message";
|
||||
const body = prefs.messagePreview ? (message.text?.trim() || messagePreviewByType(message.type)) : "New message";
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
tag: `chat-${chatId}`,
|
||||
});
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
const store = useChatStore.getState();
|
||||
store.setActiveChatId(chatId);
|
||||
store.setFocusedMessage(chatId, message.id);
|
||||
notification.close();
|
||||
};
|
||||
}
|
||||
|
||||
function messagePreviewByType(type: Message["type"]): string {
|
||||
if (type === "image") return "Photo";
|
||||
if (type === "video") return "Video";
|
||||
if (type === "audio") return "Audio";
|
||||
if (type === "voice") return "Voice message";
|
||||
if (type === "file") return "File";
|
||||
if (type === "circle_video") return "Video message";
|
||||
return "New message";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user