feat: add SMTP email support and fix profile/permissions bugs
This commit is contained in:
+62
-45
@@ -5,6 +5,7 @@ import { useServerStore } from './server.ts';
|
||||
import { usePresenceStore } from './presence.ts';
|
||||
import { useTypingStore } from './typing.ts';
|
||||
import { useVoiceStore } from './voice.ts';
|
||||
import { useAuthStore } from './auth.ts';
|
||||
import type { Message } from './message.ts';
|
||||
import type { Channel } from './channel.ts';
|
||||
import type { Server } from './server.ts';
|
||||
@@ -14,7 +15,7 @@ type UnknownPayload = Record<string, unknown>;
|
||||
|
||||
export interface WsEvent {
|
||||
type: string;
|
||||
payload: UnknownPayload;
|
||||
data?: UnknownPayload;
|
||||
}
|
||||
|
||||
interface WebSocketState {
|
||||
@@ -25,6 +26,17 @@ interface WebSocketState {
|
||||
send: (event: WsEvent) => void;
|
||||
}
|
||||
|
||||
let unreadCount = 0;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('focus', () => {
|
||||
unreadCount = 0;
|
||||
if (document.title.match(/^\(\d+\)\s/)) {
|
||||
document.title = document.title.replace(/^\(\d+\)\s/, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getWsHost(): string {
|
||||
const { protocol, host } = window.location;
|
||||
const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -35,32 +47,13 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function extractMessage(payload: UnknownPayload): Message | null {
|
||||
if (!isRecord(payload.message)) return null;
|
||||
return payload.message as unknown as Message;
|
||||
}
|
||||
|
||||
function extractChannel(payload: UnknownPayload): Channel | null {
|
||||
if (!isRecord(payload.channel)) return null;
|
||||
return payload.channel as unknown as Channel;
|
||||
}
|
||||
|
||||
function extractServer(payload: UnknownPayload): Server | null {
|
||||
if (!isRecord(payload.server)) return null;
|
||||
return payload.server as unknown as Server;
|
||||
}
|
||||
|
||||
function extractIds(payload: UnknownPayload): { channel_id: string; message_id: string } | null {
|
||||
if (typeof payload.channel_id !== 'string' || typeof payload.message_id !== 'string') {
|
||||
function extractIds(payload: UnknownPayload | undefined): { channel_id: string; message_id: string } | null {
|
||||
if (!payload || typeof payload.channel_id !== 'string' || typeof payload.message_id !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return { channel_id: payload.channel_id, message_id: payload.message_id };
|
||||
}
|
||||
|
||||
function extractChannelId(payload: UnknownPayload): string | null {
|
||||
return typeof payload.channel_id === 'string' ? payload.channel_id : null;
|
||||
}
|
||||
|
||||
export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
socket: null,
|
||||
connected: false,
|
||||
@@ -115,7 +108,7 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
}
|
||||
|
||||
if (data.type === 'ping') {
|
||||
get().send({ type: 'pong', payload: {} });
|
||||
get().send({ type: 'pong', data: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,51 +120,75 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
const removeChannel = useChannelStore.getState().removeChannel;
|
||||
const updateServer = useServerStore.getState().updateServer;
|
||||
|
||||
const payload = data.data || {};
|
||||
|
||||
switch (data.type) {
|
||||
case 'message': {
|
||||
const msg = extractMessage(data.payload);
|
||||
if (msg) addMessage(msg);
|
||||
case 'MESSAGE_CREATE': {
|
||||
if (isRecord(payload)) {
|
||||
const msg = payload as unknown as Message;
|
||||
addMessage(msg);
|
||||
|
||||
// Desktop notification
|
||||
const currentUserId = useAuthStore.getState().user?.id;
|
||||
if (msg.author_id !== currentUserId && !document.hasFocus()) {
|
||||
// Update browser tab title
|
||||
unreadCount++;
|
||||
if (document.title.match(/^\(\d+\)\s/)) {
|
||||
document.title = document.title.replace(/^\(\d+\)\s/, `(${unreadCount}) `);
|
||||
} else {
|
||||
document.title = `(${unreadCount}) ` + document.title;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
const title = msg.author_username ? `New message from ${msg.author_display_name || msg.author_username}` : 'New message';
|
||||
const notification = new Notification(title, {
|
||||
body: msg.content,
|
||||
icon: '/icons/icon-192.png',
|
||||
});
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'message_updated': {
|
||||
const msg = extractMessage(data.payload);
|
||||
if (msg) updateMessage(msg);
|
||||
case 'MESSAGE_UPDATE': {
|
||||
if (isRecord(payload)) updateMessage(payload as unknown as Message);
|
||||
break;
|
||||
}
|
||||
case 'message_deleted': {
|
||||
const ids = extractIds(data.payload);
|
||||
case 'MESSAGE_DELETE': {
|
||||
const ids = extractIds(payload);
|
||||
if (ids) removeMessage(ids.channel_id, ids.message_id);
|
||||
break;
|
||||
}
|
||||
case 'channel_created': {
|
||||
const channel = extractChannel(data.payload);
|
||||
if (channel) addChannel(channel);
|
||||
case 'CHANNEL_CREATE': {
|
||||
if (isRecord(payload)) addChannel(payload as unknown as Channel);
|
||||
break;
|
||||
}
|
||||
case 'channel_updated': {
|
||||
const channel = extractChannel(data.payload);
|
||||
if (channel) updateChannel(channel);
|
||||
case 'CHANNEL_UPDATE': {
|
||||
if (isRecord(payload)) updateChannel(payload as unknown as Channel);
|
||||
break;
|
||||
}
|
||||
case 'channel_deleted': {
|
||||
const channelId = extractChannelId(data.payload);
|
||||
case 'CHANNEL_DELETE': {
|
||||
const channelId = typeof payload.channel_id === 'string' ? payload.channel_id : null;
|
||||
if (channelId) removeChannel(channelId);
|
||||
break;
|
||||
}
|
||||
case 'server_updated': {
|
||||
const server = extractServer(data.payload);
|
||||
if (server) updateServer(server);
|
||||
case 'SERVER_UPDATE': {
|
||||
if (isRecord(payload)) updateServer(payload as unknown as Server);
|
||||
break;
|
||||
}
|
||||
case 'PRESENCE_UPDATE': {
|
||||
const { user_id, username, status } = data.payload as Record<string, string>;
|
||||
const { user_id, username, status } = payload as Record<string, string>;
|
||||
if (user_id && username && status) {
|
||||
usePresenceStore.getState().updatePresence(user_id, username, status as UserStatus);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TYPING_START': {
|
||||
const { channel_id, user_id, username } = data.payload as Record<string, string>;
|
||||
const { channel_id, user_id, username } = payload as Record<string, string>;
|
||||
if (channel_id && user_id && username) {
|
||||
useTypingStore.getState()._handleTypingEvent({ channel_id, user_id, username });
|
||||
}
|
||||
@@ -179,7 +196,7 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
}
|
||||
case 'VOICE_WHISPER': {
|
||||
// Forwarded from backend — only intended recipient gets this
|
||||
const { from_user_id, from_username, message } = data.payload as Record<string, string>;
|
||||
const { from_user_id, from_username, message } = payload as Record<string, string>;
|
||||
useVoiceStore.getState()._addWhisper({
|
||||
fromUserId: from_user_id || '',
|
||||
fromUsername: from_username || 'unknown',
|
||||
|
||||
Reference in New Issue
Block a user