mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(mentions): client api types, store, snapshot + message hooks
This commit is contained in:
@@ -17,6 +17,7 @@ export * from './hc-center';
|
|||||||
export * from './help';
|
export * from './help';
|
||||||
export * from './housekeeping';
|
export * from './housekeeping';
|
||||||
export * from './inventory';
|
export * from './inventory';
|
||||||
|
export * from './mentions';
|
||||||
export * from './mod-tools';
|
export * from './mod-tools';
|
||||||
export * from './navigator';
|
export * from './navigator';
|
||||||
export * from './nitro';
|
export * from './nitro';
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export interface IMentionEntry
|
||||||
|
{
|
||||||
|
mentionId: number;
|
||||||
|
senderId: number;
|
||||||
|
senderUsername: string;
|
||||||
|
roomId: number;
|
||||||
|
roomName: string;
|
||||||
|
message: string;
|
||||||
|
mentionType: number;
|
||||||
|
timestamp: number;
|
||||||
|
read: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export class MentionType
|
||||||
|
{
|
||||||
|
public static DIRECT: number = 0;
|
||||||
|
public static ROOM: number = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './MentionType';
|
||||||
|
export * from './IMentionEntry';
|
||||||
@@ -11,6 +11,7 @@ export * from './groups';
|
|||||||
export * from './help';
|
export * from './help';
|
||||||
export * from './housekeeping';
|
export * from './housekeeping';
|
||||||
export * from './inventory';
|
export * from './inventory';
|
||||||
|
export * from './mentions';
|
||||||
export * from './mod-tools';
|
export * from './mod-tools';
|
||||||
export * from './navigator';
|
export * from './navigator';
|
||||||
export * from './notification';
|
export * from './notification';
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from 'vitest';
|
||||||
|
import { addMention, setMentions, markAllRead, markRead, getMentionsSnapshot, getUnreadCount, resetMentions } from '../mentionsStore';
|
||||||
|
import { IMentionEntry } from '../../../api/mentions';
|
||||||
|
|
||||||
|
const make = (id: number, read = false): IMentionEntry => ({
|
||||||
|
mentionId: id, senderId: 1, senderUsername: 'Bob', roomId: 9, roomName: 'R',
|
||||||
|
message: '@me hi', mentionType: 0, timestamp: 0, read
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mentionsStore', () =>
|
||||||
|
{
|
||||||
|
beforeEach(() => resetMentions());
|
||||||
|
|
||||||
|
it('adds newest-first and counts unread', () =>
|
||||||
|
{
|
||||||
|
addMention(make(1));
|
||||||
|
addMention(make(2));
|
||||||
|
expect(getMentionsSnapshot()[0].mentionId).toBe(2);
|
||||||
|
expect(getUnreadCount()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setMentions replaces and recomputes unread', () =>
|
||||||
|
{
|
||||||
|
setMentions([make(1, true), make(2, false)]);
|
||||||
|
expect(getMentionsSnapshot()).toHaveLength(2);
|
||||||
|
expect(getUnreadCount()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('markAllRead zeroes unread', () =>
|
||||||
|
{
|
||||||
|
setMentions([make(1), make(2)]);
|
||||||
|
markAllRead();
|
||||||
|
expect(getUnreadCount()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('markRead clears a single entry', () =>
|
||||||
|
{
|
||||||
|
setMentions([make(1), make(2)]);
|
||||||
|
markRead(1);
|
||||||
|
expect(getUnreadCount()).toBe(1);
|
||||||
|
expect(getMentionsSnapshot().find(m => m.mentionId === 1)!.read).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './useMentionsSnapshot';
|
||||||
|
export * from './useMentionMessages';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { IMentionEntry } from '../../api/mentions';
|
||||||
|
|
||||||
|
let mentions: IMentionEntry[] = [];
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
const emit = () => { for(const l of listeners) l(); };
|
||||||
|
|
||||||
|
export const subscribeMentions = (onChange: () => void): (() => void) =>
|
||||||
|
{
|
||||||
|
listeners.add(onChange);
|
||||||
|
return () => { listeners.delete(onChange); };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentionsSnapshot = (): ReadonlyArray<IMentionEntry> => mentions;
|
||||||
|
|
||||||
|
export const getUnreadCount = (): number => mentions.reduce((n, m) => n + (m.read ? 0 : 1), 0);
|
||||||
|
|
||||||
|
export const setMentions = (list: IMentionEntry[]): void =>
|
||||||
|
{
|
||||||
|
mentions = [...list].sort((a, b) => b.mentionId - a.mentionId);
|
||||||
|
emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMention = (entry: IMentionEntry): void =>
|
||||||
|
{
|
||||||
|
if(mentions.some(m => m.mentionId === entry.mentionId && entry.mentionId !== 0)) return;
|
||||||
|
mentions = [entry, ...mentions];
|
||||||
|
emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markRead = (mentionId: number): void =>
|
||||||
|
{
|
||||||
|
mentions = mentions.map(m => m.mentionId === mentionId ? { ...m, read: true } : m);
|
||||||
|
emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markAllRead = (): void =>
|
||||||
|
{
|
||||||
|
mentions = mentions.map(m => m.read ? m : { ...m, read: true });
|
||||||
|
emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetMentions = (): void => { mentions = []; emit(); };
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { MarkMentionsReadComposer, MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer, SoundNames } from '../../api';
|
||||||
|
import { useMessageEvent } from '../events';
|
||||||
|
import { useNotificationActions } from '../notification';
|
||||||
|
import { addMention, setMentions } from './mentionsStore';
|
||||||
|
|
||||||
|
// MarkMentionsReadComposer is part of the mentions wire contract; it is sent by
|
||||||
|
// the UI layer (later phase) when the user opens / clears the mentions window.
|
||||||
|
void MarkMentionsReadComposer;
|
||||||
|
|
||||||
|
export const useMentionMessages = (): void =>
|
||||||
|
{
|
||||||
|
const { showSingleBubble } = useNotificationActions();
|
||||||
|
|
||||||
|
const onMentionsList = useCallback((event: MentionsListEvent) =>
|
||||||
|
{
|
||||||
|
const list = event.getParser().mentions;
|
||||||
|
|
||||||
|
setMentions(list.map(m => ({
|
||||||
|
mentionId: m.mentionId,
|
||||||
|
senderId: m.senderId,
|
||||||
|
senderUsername: m.senderUsername,
|
||||||
|
roomId: m.roomId,
|
||||||
|
roomName: m.roomName,
|
||||||
|
message: m.message,
|
||||||
|
mentionType: m.mentionType,
|
||||||
|
timestamp: m.timestamp,
|
||||||
|
read: m.read
|
||||||
|
})));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMentionReceived = useCallback((event: MentionReceivedEvent) =>
|
||||||
|
{
|
||||||
|
if(!GetConfigurationValue<boolean>('mentions_ui.enabled', true)) return;
|
||||||
|
|
||||||
|
const m = event.getParser().mention;
|
||||||
|
|
||||||
|
const entry: IMentionEntry = {
|
||||||
|
mentionId: m.mentionId,
|
||||||
|
senderId: m.senderId,
|
||||||
|
senderUsername: m.senderUsername,
|
||||||
|
roomId: m.roomId,
|
||||||
|
roomName: m.roomName,
|
||||||
|
message: m.message,
|
||||||
|
mentionType: m.mentionType,
|
||||||
|
timestamp: m.timestamp,
|
||||||
|
read: false
|
||||||
|
};
|
||||||
|
|
||||||
|
addMention(entry);
|
||||||
|
|
||||||
|
if(GetConfigurationValue<boolean>('mentions_ui.sound', true)) PlaySound(SoundNames.MESSENGER_MESSAGE_RECEIVED);
|
||||||
|
|
||||||
|
showSingleBubble(
|
||||||
|
LocalizeText('mentions.notification', [ 'sender', 'room' ], [ entry.senderUsername, entry.roomName ]),
|
||||||
|
NotificationBubbleType.INFO,
|
||||||
|
null,
|
||||||
|
'mentions/toggle',
|
||||||
|
entry.senderUsername
|
||||||
|
);
|
||||||
|
}, [ showSingleBubble ]);
|
||||||
|
|
||||||
|
useMessageEvent<MentionsListEvent>(MentionsListEvent, onMentionsList);
|
||||||
|
useMessageEvent<MentionReceivedEvent>(MentionReceivedEvent, onMentionReceived);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(!GetConfigurationValue<boolean>('mentions_ui.enabled', true)) return;
|
||||||
|
|
||||||
|
SendMessageComposer(new RequestMentionsComposer());
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { IMentionEntry } from '../../api/mentions';
|
||||||
|
import { useExternalSnapshot } from '../events/useExternalSnapshot';
|
||||||
|
import { getMentionsSnapshot, getUnreadCount, subscribeMentions } from './mentionsStore';
|
||||||
|
|
||||||
|
export const useMentionsSnapshot = (): { mentions: ReadonlyArray<IMentionEntry>; unreadCount: number } =>
|
||||||
|
{
|
||||||
|
const mentions = useExternalSnapshot(subscribeMentions, getMentionsSnapshot);
|
||||||
|
const unreadCount = useExternalSnapshot(subscribeMentions, getUnreadCount);
|
||||||
|
return { mentions, unreadCount };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user