feat: Add chat seperator and restore last viewed chat on restart

This commit is contained in:
2026-04-02 00:47:44 +02:00
parent bbb6deb0a2
commit 5d7e045764
10 changed files with 412 additions and 18 deletions

View File

@@ -2,7 +2,10 @@
/* eslint-disable id-length */
/* eslint-disable @typescript-eslint/no-unused-vars,, complexity */
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import {
NavigationEnd,
Router
} from '@angular/router';
import {
Actions,
createEffect,
@@ -15,7 +18,8 @@ import {
import {
of,
from,
EMPTY
EMPTY,
merge
} from 'rxjs';
import {
map,
@@ -38,7 +42,12 @@ import {
selectSavedRooms
} from './rooms.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
import {
clearLastViewedChatFromStorage,
DatabaseService,
loadLastViewedChatFromStorage,
saveLastViewedChatToStorage
} from '../../infrastructure/persistence';
import {
CLIENT_UPDATE_REQUIRED_MESSAGE,
type ServerSourceSelector,
@@ -137,6 +146,19 @@ function resolveRoomChannels(
return undefined;
}
function resolveTextChannelId(
channels: Room['channels'] | undefined,
preferredChannelId?: string | null
): string | null {
const textChannels = (channels ?? []).filter((channel) => channel.type === 'text');
if (preferredChannelId && textChannels.some((channel) => channel.id === preferredChannelId)) {
return preferredChannelId;
}
return textChannels[0]?.id ?? null;
}
interface RoomPresenceSignalingMessage {
type: string;
reason?: string;
@@ -183,6 +205,51 @@ export class RoomsEffects {
)
);
/** Opens the routed room after login/refresh once saved rooms are available. */
syncViewedRoomToRoute$ = createEffect(() =>
merge(
this.actions$.pipe(
ofType(
RoomsActions.loadRoomsSuccess,
UsersActions.loadCurrentUserSuccess,
UsersActions.setCurrentUser
)
),
this.router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd)
)
).pipe(
withLatestFrom(
this.store.select(selectCurrentUser),
this.store.select(selectCurrentRoom),
this.store.select(selectSavedRooms)
),
mergeMap(([
, currentUser,
currentRoom,
savedRooms
]) => {
if (!currentUser) {
return EMPTY;
}
const roomId = this.extractRoomIdFromUrl(this.router.url);
if (!roomId || currentRoom?.id === roomId) {
return EMPTY;
}
const room = savedRooms.find((savedRoom) => savedRoom.id === roomId) ?? null;
if (!room) {
return EMPTY;
}
return of(RoomsActions.viewServer({ room }));
})
)
);
/** Searches the server directory with debounced input. */
searchServers$ = createEffect(() =>
this.actions$.pipe(
@@ -431,6 +498,106 @@ export class RoomsEffects {
{ dispatch: false }
);
/** Remembers the viewed room whenever a room becomes active. */
persistLastViewedChatOnRoomActivation$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.createRoomSuccess, RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
withLatestFrom(this.store.select(selectCurrentUser)),
tap(([
{ room },
currentUser
]) => {
if (!currentUser) {
return;
}
const persisted = loadLastViewedChatFromStorage(currentUser.id);
const channelId = persisted?.roomId === room.id
? resolveTextChannelId(room.channels, persisted.channelId)
: resolveTextChannelId(room.channels);
saveLastViewedChatToStorage({
userId: currentUser.id,
roomId: room.id,
channelId
});
})
),
{ dispatch: false }
);
/** Remembers the currently selected text channel for the active room. */
persistLastViewedChatOnChannelSelection$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.selectChannel),
withLatestFrom(this.store.select(selectCurrentRoom), this.store.select(selectCurrentUser)),
tap(([
{ channelId },
currentRoom,
currentUser
]) => {
if (!currentRoom || !currentUser) {
return;
}
const resolvedChannelId = resolveTextChannelId(currentRoom.channels, channelId);
if (!resolvedChannelId || resolvedChannelId !== channelId) {
return;
}
saveLastViewedChatToStorage({
userId: currentUser.id,
roomId: currentRoom.id,
channelId
});
})
),
{ dispatch: false }
);
/** Restores the last viewed text channel once the active room's channels are known. */
restoreLastViewedTextChannel$ = createEffect(() =>
this.actions$.pipe(
ofType(
RoomsActions.createRoomSuccess,
RoomsActions.joinRoomSuccess,
RoomsActions.viewServerSuccess,
RoomsActions.updateRoom
),
withLatestFrom(
this.store.select(selectCurrentUser),
this.store.select(selectCurrentRoom),
this.store.select(selectActiveChannelId)
),
mergeMap(([
, currentUser,
currentRoom,
activeChannelId
]) => {
if (!currentUser || !currentRoom) {
return EMPTY;
}
const persisted = loadLastViewedChatFromStorage(currentUser.id);
if (!persisted || persisted.roomId !== currentRoom.id) {
return EMPTY;
}
const channelId = resolveTextChannelId(currentRoom.channels, persisted.channelId);
if (!channelId || channelId === activeChannelId) {
return EMPTY;
}
return of(RoomsActions.selectChannel({ channelId }));
})
)
);
refreshServerOwnedRoomMetadata$ = createEffect(() =>
this.actions$.pipe(
ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
@@ -648,6 +815,22 @@ export class RoomsEffects {
)
);
/** Clears stale resume state when the remembered room is removed locally. */
clearLastViewedChatOnRoomRemoval$ = createEffect(
() =>
this.actions$.pipe(
ofType(RoomsActions.deleteRoomSuccess, RoomsActions.forgetRoomSuccess),
tap(({ roomId }) => {
const persisted = loadLastViewedChatFromStorage();
if (persisted?.roomId === roomId) {
clearLastViewedChatFromStorage();
}
})
),
{ dispatch: false }
);
/** Updates room settings (host/admin-only) and broadcasts changes to all peers. */
updateRoomSettings$ = createEffect(() =>
this.actions$.pipe(