fix: solve small pm chat ui issues
unwrap the pill fix the fetching images in pm not auto download
This commit is contained in:
@@ -149,6 +149,7 @@ The domain must avoid marking an entire historical backlog as unread the first t
|
||||
|
||||
- When `syncRoomCatalog()` sees a room for the first time, its baseline is set to `Date.now()`. Old stored messages stay treated as historical backlog.
|
||||
- When a live message arrives before the room has been catalogued, `ensureRoomTracking()` uses `message.timestamp - 1` so that the current live message still counts as unread.
|
||||
- When startup room metadata has not loaded channels yet, existing channel read markers are preserved until the real text-channel catalog arrives, preventing previously read messages from reappearing as unread after reload.
|
||||
|
||||
### Channel scope
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Injector,
|
||||
runInInjectionContext,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import type {
|
||||
Message,
|
||||
Room,
|
||||
User
|
||||
} from '../../../../shared-kernel';
|
||||
import { NotificationAudioService } from '../../../../core/services/notification-audio.service';
|
||||
import { DatabaseService } from '../../../../infrastructure/persistence';
|
||||
import {
|
||||
selectActiveChannelId,
|
||||
selectCurrentRoom,
|
||||
selectSavedRooms
|
||||
} from '../../../../store/rooms/rooms.selectors';
|
||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import { DesktopNotificationService } from '../../infrastructure/services/desktop-notification.service';
|
||||
import { NotificationSettingsStorageService } from '../../infrastructure/services/notification-settings-storage.service';
|
||||
import { createDefaultNotificationSettings, type NotificationsSettings } from '../../domain/models/notification.model';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
|
||||
const alice: User = {
|
||||
id: 'alice',
|
||||
oderId: 'alice',
|
||||
username: 'alice',
|
||||
displayName: 'Alice',
|
||||
status: 'online',
|
||||
role: 'member',
|
||||
joinedAt: 1
|
||||
};
|
||||
|
||||
describe('NotificationsService', () => {
|
||||
it('keeps channel read markers when startup room metadata has no channels yet', async () => {
|
||||
const roomWithoutChannels = createRoom({ channels: [] });
|
||||
const context = createServiceContext({
|
||||
currentUser: alice,
|
||||
savedRooms: [roomWithoutChannels],
|
||||
settings: {
|
||||
...createDefaultNotificationSettings(),
|
||||
roomBaselines: { 'room-1': 10 },
|
||||
lastReadByChannel: {
|
||||
'room-1': {
|
||||
'channel-1': 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await context.service.initialize();
|
||||
|
||||
expect(context.service.settings().lastReadByChannel['room-1']?.['channel-1']).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
interface ServiceContextOptions {
|
||||
currentUser: User | null;
|
||||
savedRooms: Room[];
|
||||
settings: NotificationsSettings;
|
||||
}
|
||||
|
||||
interface ServiceContext {
|
||||
service: NotificationsService;
|
||||
}
|
||||
|
||||
function createServiceContext(options: ServiceContextOptions): ServiceContext {
|
||||
const currentUser = signal<User | null>(options.currentUser);
|
||||
const savedRooms = signal<Room[]>(options.savedRooms);
|
||||
const currentRoom = signal<Room | null>(null);
|
||||
const activeChannelId = signal<string | null>(null);
|
||||
|
||||
let storedSettings = options.settings;
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
getMessagesSince: vi.fn(async (): Promise<Message[]> => [])
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: DesktopNotificationService,
|
||||
useValue: {
|
||||
clearAttention: vi.fn(),
|
||||
onWindowStateChanged: vi.fn(() => () => undefined),
|
||||
requestAttention: vi.fn(),
|
||||
showNotification: vi.fn(async () => undefined)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: NotificationAudioService,
|
||||
useValue: {
|
||||
play: vi.fn()
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: NotificationSettingsStorageService,
|
||||
useValue: {
|
||||
load: vi.fn(() => storedSettings),
|
||||
save: vi.fn((settings: NotificationsSettings) => {
|
||||
storedSettings = settings;
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: Store,
|
||||
useValue: {
|
||||
selectSignal: vi.fn((selector: unknown) => {
|
||||
if (selector === selectCurrentRoom) {
|
||||
return currentRoom;
|
||||
}
|
||||
|
||||
if (selector === selectActiveChannelId) {
|
||||
return activeChannelId;
|
||||
}
|
||||
|
||||
if (selector === selectSavedRooms) {
|
||||
return savedRooms;
|
||||
}
|
||||
|
||||
if (selector === selectCurrentUser) {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
throw new Error('Unexpected selector requested by NotificationsService test.');
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
service: runInInjectionContext(injector, () => new NotificationsService())
|
||||
};
|
||||
}
|
||||
|
||||
function createRoom(overrides: Partial<Room> = {}): Room {
|
||||
return {
|
||||
id: 'room-1',
|
||||
name: 'Room 1',
|
||||
description: '',
|
||||
channels: [
|
||||
{
|
||||
id: 'channel-1',
|
||||
name: 'general',
|
||||
type: 'text'
|
||||
}
|
||||
],
|
||||
members: [],
|
||||
roles: [],
|
||||
...overrides
|
||||
} as Room;
|
||||
}
|
||||
@@ -102,19 +102,24 @@ export class NotificationsService {
|
||||
nextSettings.mutedRooms[room.id] = currentSettings.mutedRooms[room.id] === true;
|
||||
nextSettings.roomBaselines[room.id] = currentSettings.roomBaselines[room.id] ?? now;
|
||||
|
||||
const hasChannelCatalog = (room.channels?.length ?? 0) > 0;
|
||||
const textChannelIds = new Set(getRoomTextChannelIds(room));
|
||||
const mutedChannels = currentSettings.mutedChannels[room.id] ?? {};
|
||||
const lastReadByChannel = currentSettings.lastReadByChannel[room.id] ?? {};
|
||||
|
||||
nextSettings.mutedChannels[room.id] = Object.fromEntries(
|
||||
Object.entries(mutedChannels)
|
||||
.filter(([channelId, muted]) => textChannelIds.has(channelId) && muted === true)
|
||||
);
|
||||
nextSettings.mutedChannels[room.id] = hasChannelCatalog
|
||||
? Object.fromEntries(
|
||||
Object.entries(mutedChannels)
|
||||
.filter(([channelId, muted]) => textChannelIds.has(channelId) && muted === true)
|
||||
)
|
||||
: { ...mutedChannels };
|
||||
|
||||
nextSettings.lastReadByChannel[room.id] = Object.fromEntries(
|
||||
Object.entries(lastReadByChannel)
|
||||
.filter((entry): entry is [string, number] => textChannelIds.has(entry[0]) && typeof entry[1] === 'number')
|
||||
);
|
||||
nextSettings.lastReadByChannel[room.id] = hasChannelCatalog
|
||||
? Object.fromEntries(
|
||||
Object.entries(lastReadByChannel)
|
||||
.filter((entry): entry is [string, number] => textChannelIds.has(entry[0]) && typeof entry[1] === 'number')
|
||||
)
|
||||
: { ...lastReadByChannel };
|
||||
}
|
||||
|
||||
this.setSettings(nextSettings);
|
||||
|
||||
Reference in New Issue
Block a user