feat: Add chat seperator and restore last viewed chat on restart
This commit is contained in:
@@ -18,7 +18,11 @@ import {
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { DatabaseService } from './infrastructure/persistence';
|
||||
import {
|
||||
DatabaseService,
|
||||
loadGeneralSettingsFromStorage,
|
||||
loadLastViewedChatFromStorage
|
||||
} from './infrastructure/persistence';
|
||||
import { DesktopAppUpdateService } from './core/services/desktop-app-update.service';
|
||||
import { ServerDirectoryFacade } from './domains/server-directory';
|
||||
import { NotificationsFacade } from './domains/notifications';
|
||||
@@ -38,8 +42,7 @@ import { RoomsActions } from './store/rooms/rooms.actions';
|
||||
import { selectCurrentRoom } from './store/rooms/rooms.selectors';
|
||||
import {
|
||||
ROOM_URL_PATTERN,
|
||||
STORAGE_KEY_CURRENT_USER_ID,
|
||||
STORAGE_KEY_LAST_VISITED_ROUTE
|
||||
STORAGE_KEY_CURRENT_USER_ID
|
||||
} from './core/constants';
|
||||
import {
|
||||
ThemeNodeDirective,
|
||||
@@ -222,14 +225,16 @@ export class App implements OnInit, OnDestroy {
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
const last = localStorage.getItem(STORAGE_KEY_LAST_VISITED_ROUTE);
|
||||
const current = this.router.url;
|
||||
const generalSettings = loadGeneralSettingsFromStorage();
|
||||
const lastViewedChat = loadLastViewedChatFromStorage(currentUserId);
|
||||
|
||||
if (last && typeof last === 'string') {
|
||||
const current = this.router.url;
|
||||
|
||||
if (current === '/' || current === '/search') {
|
||||
this.router.navigate([last], { replaceUrl: true }).catch(() => {});
|
||||
}
|
||||
if (
|
||||
generalSettings.reopenLastViewedChat
|
||||
&& lastViewedChat
|
||||
&& (current === '/' || current === '/search')
|
||||
) {
|
||||
this.router.navigate(['/room', lastViewedChat.roomId], { replaceUrl: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +242,6 @@ export class App implements OnInit, OnDestroy {
|
||||
if (evt instanceof NavigationEnd) {
|
||||
const url = evt.urlAfterRedirects || evt.url;
|
||||
|
||||
localStorage.setItem(STORAGE_KEY_LAST_VISITED_ROUTE, url);
|
||||
|
||||
const roomMatch = url.match(ROOM_URL_PATTERN);
|
||||
const currentRoomId = roomMatch ? roomMatch[1] : null;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const STORAGE_KEY_CURRENT_USER_ID = 'metoyou_currentUserId';
|
||||
export const STORAGE_KEY_LAST_VISITED_ROUTE = 'metoyou_lastVisitedRoute';
|
||||
export const STORAGE_KEY_GENERAL_SETTINGS = 'metoyou_general_settings';
|
||||
export const STORAGE_KEY_LAST_VIEWED_CHAT = 'metoyou_lastViewedChat';
|
||||
export const STORAGE_KEY_CONNECTION_SETTINGS = 'metoyou_connection_settings';
|
||||
export const STORAGE_KEY_NOTIFICATION_SETTINGS = 'metoyou_notification_settings';
|
||||
export const STORAGE_KEY_VOICE_SETTINGS = 'metoyou_voice_settings';
|
||||
|
||||
@@ -37,7 +37,17 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (message of messages(); track message.id) {
|
||||
@for (message of messages(); track message.id; let index = $index) {
|
||||
@if (dateSeparatorLabels().get(index); as separatorLabel) {
|
||||
<div class="flex items-center gap-3 py-1">
|
||||
<div class="h-px flex-1 bg-border"></div>
|
||||
<span class="rounded-full border border-border bg-background/90 px-3 py-1 text-xs font-medium text-muted-foreground shadow-sm">
|
||||
{{ separatorLabel }}
|
||||
</span>
|
||||
<div class="h-px flex-1 bg-border"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-chat-message-item
|
||||
[message]="message"
|
||||
[repliedMessage]="findRepliedMessage(message.replyToId)"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { Attachment } from '../../../../../attachment';
|
||||
import { getMessageTimestamp } from '../../../../domain/message.rules';
|
||||
import { Message } from '../../../../../../shared-kernel';
|
||||
import {
|
||||
ChatMessageDeleteEvent,
|
||||
@@ -45,6 +46,12 @@ declare global {
|
||||
export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
|
||||
@ViewChild('messagesContainer') messagesContainer?: ElementRef<HTMLDivElement>;
|
||||
|
||||
private readonly dateSeparatorFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
readonly allMessages = input.required<Message[]>();
|
||||
readonly channelMessages = input.required<Message[]>();
|
||||
readonly loading = input(false);
|
||||
@@ -83,6 +90,23 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
|
||||
() => this.channelMessages().length > this.displayLimit()
|
||||
);
|
||||
|
||||
readonly dateSeparatorLabels = computed(() => {
|
||||
const labels = new Map<number, string>();
|
||||
let previousDayKey: string | null = null;
|
||||
|
||||
this.messages().forEach((message, index) => {
|
||||
const timestamp = this.getMessageDateTimestamp(message);
|
||||
const currentDayKey = this.getMessageDayKey(timestamp);
|
||||
|
||||
if (currentDayKey !== previousDayKey) {
|
||||
labels.set(index, this.dateSeparatorFormatter.format(new Date(timestamp)));
|
||||
previousDayKey = currentDayKey;
|
||||
}
|
||||
});
|
||||
|
||||
return labels;
|
||||
});
|
||||
|
||||
private initialScrollObserver: MutationObserver | null = null;
|
||||
private initialScrollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private boundOnImageLoad: (() => void) | null = null;
|
||||
@@ -342,6 +366,16 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private getMessageDateTimestamp(message: Message): number {
|
||||
return message.timestamp || getMessageTimestamp(message);
|
||||
}
|
||||
|
||||
private getMessageDayKey(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
}
|
||||
|
||||
private scrollToBottomSmooth(): void {
|
||||
const element = this.messagesContainer?.nativeElement;
|
||||
|
||||
|
||||
@@ -9,6 +9,29 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Reopen last chat on launch</p>
|
||||
<p class="text-xs text-muted-foreground">Open the same server and text channel the next time MetoYou starts.</p>
|
||||
</div>
|
||||
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="reopenLastViewedChat()"
|
||||
(change)="onReopenLastViewedChatChange($event)"
|
||||
id="general-reopen-last-chat-toggle"
|
||||
aria-label="Toggle reopen last chat on launch"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
class="w-10 h-5 bg-secondary rounded-full peer peer-checked:bg-primary peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-border bg-secondary/20 p-4 transition-opacity"
|
||||
[class.opacity-60]="!isElectron"
|
||||
|
||||
@@ -9,6 +9,10 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucidePower } from '@ng-icons/lucide';
|
||||
|
||||
import type { DesktopSettingsSnapshot } from '../../../../core/platform/electron/electron-api.models';
|
||||
import {
|
||||
loadGeneralSettingsFromStorage,
|
||||
saveGeneralSettingsToStorage
|
||||
} from '../../../../infrastructure/persistence';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { PlatformService } from '../../../../core/platform';
|
||||
|
||||
@@ -28,17 +32,29 @@ export class GeneralSettingsComponent {
|
||||
private electronBridge = inject(ElectronBridgeService);
|
||||
|
||||
readonly isElectron = this.platform.isElectron;
|
||||
reopenLastViewedChat = signal(true);
|
||||
autoStart = signal(false);
|
||||
closeToTray = signal(true);
|
||||
savingAutoStart = signal(false);
|
||||
savingCloseToTray = signal(false);
|
||||
|
||||
constructor() {
|
||||
this.loadGeneralSettings();
|
||||
|
||||
if (this.isElectron) {
|
||||
void this.loadDesktopSettings();
|
||||
}
|
||||
}
|
||||
|
||||
onReopenLastViewedChatChange(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const settings = saveGeneralSettingsToStorage({
|
||||
reopenLastViewedChat: !!input.checked
|
||||
});
|
||||
|
||||
this.reopenLastViewedChat.set(settings.reopenLastViewedChat);
|
||||
}
|
||||
|
||||
async onAutoStartChange(event: Event): Promise<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const enabled = !!input.checked;
|
||||
@@ -99,6 +115,12 @@ export class GeneralSettingsComponent {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private loadGeneralSettings(): void {
|
||||
const settings = loadGeneralSettingsFromStorage();
|
||||
|
||||
this.reopenLastViewedChat.set(settings.reopenLastViewedChat);
|
||||
}
|
||||
|
||||
private applyDesktopSettings(snapshot: DesktopSettingsSnapshot): void {
|
||||
this.autoStart.set(snapshot.autoStart);
|
||||
this.closeToTray.set(snapshot.closeToTray);
|
||||
|
||||
@@ -6,12 +6,15 @@ Offline-first storage layer that keeps messages, users, rooms, reactions, bans,
|
||||
|
||||
```
|
||||
persistence/
|
||||
├── index.ts Barrel (exports DatabaseService)
|
||||
├── app-resume.storage.ts localStorage helpers for launch settings and last viewed chat
|
||||
├── index.ts Barrel (exports DatabaseService and storage helpers)
|
||||
├── database.service.ts Platform-agnostic facade
|
||||
├── browser-database.service.ts IndexedDB backend (web)
|
||||
└── electron-database.service.ts IPC/SQLite backend (desktop)
|
||||
```
|
||||
|
||||
`app-resume.storage.ts` is the one exception to the `DatabaseService` facade. It stores lightweight UI-level launch preferences and the last viewed room/channel snapshot in `localStorage`, which would be unnecessary overhead to route through IndexedDB or SQLite.
|
||||
|
||||
## Platform routing
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
STORAGE_KEY_GENERAL_SETTINGS,
|
||||
STORAGE_KEY_LAST_VIEWED_CHAT
|
||||
} from '../../core/constants';
|
||||
|
||||
export interface GeneralSettings {
|
||||
reopenLastViewedChat: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = {
|
||||
reopenLastViewedChat: true
|
||||
};
|
||||
|
||||
export interface LastViewedChatSnapshot {
|
||||
userId: string;
|
||||
roomId: string;
|
||||
channelId: string | null;
|
||||
}
|
||||
|
||||
export function loadGeneralSettingsFromStorage(): GeneralSettings {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_GENERAL_SETTINGS);
|
||||
|
||||
if (!raw) {
|
||||
return { ...DEFAULT_GENERAL_SETTINGS };
|
||||
}
|
||||
|
||||
return normaliseGeneralSettings(JSON.parse(raw) as Partial<GeneralSettings>);
|
||||
} catch {
|
||||
return { ...DEFAULT_GENERAL_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
export function saveGeneralSettingsToStorage(patch: Partial<GeneralSettings>): GeneralSettings {
|
||||
const nextSettings = normaliseGeneralSettings({
|
||||
...loadGeneralSettingsFromStorage(),
|
||||
...patch
|
||||
});
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY_GENERAL_SETTINGS, JSON.stringify(nextSettings));
|
||||
} catch {}
|
||||
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
export function loadLastViewedChatFromStorage(userId?: string | null): LastViewedChatSnapshot | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_LAST_VIEWED_CHAT);
|
||||
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = normaliseLastViewedChatSnapshot(JSON.parse(raw) as Partial<LastViewedChatSnapshot>);
|
||||
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userId && snapshot.userId !== userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLastViewedChatToStorage(snapshot: LastViewedChatSnapshot): void {
|
||||
const normalised = normaliseLastViewedChatSnapshot(snapshot);
|
||||
|
||||
if (!normalised) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY_LAST_VIEWED_CHAT, JSON.stringify(normalised));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function clearLastViewedChatFromStorage(): void {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY_LAST_VIEWED_CHAT);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function normaliseGeneralSettings(raw: Partial<GeneralSettings>): GeneralSettings {
|
||||
return {
|
||||
reopenLastViewedChat:
|
||||
typeof raw.reopenLastViewedChat === 'boolean'
|
||||
? raw.reopenLastViewedChat
|
||||
: DEFAULT_GENERAL_SETTINGS.reopenLastViewedChat
|
||||
};
|
||||
}
|
||||
|
||||
function normaliseLastViewedChatSnapshot(raw: Partial<LastViewedChatSnapshot>): LastViewedChatSnapshot | null {
|
||||
const userId = typeof raw.userId === 'string' ? raw.userId.trim() : '';
|
||||
const roomId = typeof raw.roomId === 'string' ? raw.roomId.trim() : '';
|
||||
const channelId = typeof raw.channelId === 'string'
|
||||
? raw.channelId.trim() || null
|
||||
: null;
|
||||
|
||||
if (!userId || !roomId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId,
|
||||
roomId,
|
||||
channelId
|
||||
};
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './app-resume.storage';
|
||||
export * from './database.service';
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user