feat(guild-realtime): add presence and typing websocket events

This commit is contained in:
nav
2026-05-12 12:22:36 +00:00
parent 33d101af22
commit bccd942898
2 changed files with 58 additions and 1 deletions

View File

@@ -21,6 +21,14 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
server!: Server; server!: Server;
private readonly logger = new Logger(RealtimeGateway.name); private readonly logger = new Logger(RealtimeGateway.name);
private readonly onlineUsers = new Set<string>();
private userIdFromClient(client: Socket): string {
const authUser = client.handshake.auth?.userId;
const headerUser = client.handshake.headers['x-user-id'];
const userId = typeof authUser === 'string' ? authUser : Array.isArray(headerUser) ? headerUser[0] : headerUser;
return userId && typeof userId === 'string' && userId.trim() !== '' ? userId : `anon:${client.id}`;
}
handleConnection(client: Socket): void { handleConnection(client: Socket): void {
const expected = process.env.FABRIC_API_KEY; const expected = process.env.FABRIC_API_KEY;
@@ -40,10 +48,27 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
} }
this.logger.log(`socket connected: ${client.id}`); this.logger.log(`socket connected: ${client.id}`);
const userId = this.userIdFromClient(client);
client.data.userId = userId;
this.onlineUsers.add(userId);
this.server.emit('presence.online', {
userId,
onlineCount: this.onlineUsers.size,
occurredAt: new Date().toISOString(),
});
} }
handleDisconnect(client: Socket): void { handleDisconnect(client: Socket): void {
this.logger.log(`socket disconnected: ${client.id}`); this.logger.log(`socket disconnected: ${client.id}`);
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
this.onlineUsers.delete(userId);
this.server.emit('presence.offline', {
userId,
onlineCount: this.onlineUsers.size,
occurredAt: new Date().toISOString(),
});
} }
@SubscribeMessage('join_channel') @SubscribeMessage('join_channel')
@@ -66,6 +91,38 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
return { ok: true }; return { ok: true };
} }
@SubscribeMessage('typing.start')
typingStart(
@ConnectedSocket() client: Socket,
@MessageBody() body: { channelId?: string },
): { ok: boolean } {
if (!body?.channelId) return { ok: false };
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
this.server.to(`channel:${body.channelId}`).emit('typing.start', {
channelId: body.channelId,
userId,
occurredAt: new Date().toISOString(),
});
return { ok: true };
}
@SubscribeMessage('typing.stop')
typingStop(
@ConnectedSocket() client: Socket,
@MessageBody() body: { channelId?: string },
): { ok: boolean } {
if (!body?.channelId) return { ok: false };
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
this.server.to(`channel:${body.channelId}`).emit('typing.stop', {
channelId: body.channelId,
userId,
occurredAt: new Date().toISOString(),
});
return { ok: true };
}
emitChannelEvent(channelId: string, event: string, data: Record<string, unknown>): void { emitChannelEvent(channelId: string, event: string, data: Record<string, unknown>): void {
this.server.to(`channel:${channelId}`).emit(event, data); this.server.to(`channel:${channelId}`).emit(event, data);
} }

View File

@@ -58,7 +58,7 @@
### 2.4 实时通信MVP 后半) ### 2.4 实时通信MVP 后半)
- [x] WebSocket 网关接入 - [x] WebSocket 网关接入
- [x] message.created/updated/deleted 事件广播 - [x] message.created/updated/deleted 事件广播
- [ ] 在线状态 + typing 事件 - [x] 在线状态 + typing 事件
--- ---