fix: Improve plugin ui entry points, Fix chat scroll, fix notifications, fix user rights
This commit is contained in:
@@ -23,6 +23,8 @@ direct-message/
|
||||
5. The recipient persists the message as `DELIVERED` and sends a `direct-message-status` event back.
|
||||
6. Opening the conversation marks incoming messages as `ACKNOWLEDGED` and emits a status event.
|
||||
|
||||
Incoming PM and group-chat events are ignored unless the current user is declared in the message recipients, participant profiles, or existing local conversation. Sync requests are only answered for conversation participants, so a stray peer route cannot create unread state or expose private history.
|
||||
|
||||
Status transitions are monotonic, so a stale `SENT` event cannot overwrite `DELIVERED` or `ACKNOWLEDGED`.
|
||||
|
||||
## Chat View
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
advanceDirectMessageStatus,
|
||||
createDirectConversation,
|
||||
createGroupConversation,
|
||||
directMessageEventIncludesUser,
|
||||
directMessageSyncIncludesUser,
|
||||
getDirectConversationId,
|
||||
isGroupDirectConversation,
|
||||
updateMessageStatusInConversation,
|
||||
@@ -92,6 +94,30 @@ describe('DirectMessageService domain flow', () => {
|
||||
expect(advanceDirectMessageStatus('DELIVERED', 'SENT')).toBe('DELIVERED');
|
||||
expect(advanceDirectMessageStatus('DELIVERED', 'ACKNOWLEDGED')).toBe('ACKNOWLEDGED');
|
||||
});
|
||||
|
||||
it('recognises only declared direct-message recipients and participants', () => {
|
||||
const payload = {
|
||||
message: createMessage('message-1', 'SENT', 'dm-group-test', ['bob']),
|
||||
participants: [alice, bob],
|
||||
sender: alice
|
||||
};
|
||||
|
||||
expect(directMessageEventIncludesUser(payload, 'bob')).toBe(true);
|
||||
expect(directMessageEventIncludesUser(payload, 'charlie')).toBe(false);
|
||||
});
|
||||
|
||||
it('recognises only declared sync participants', () => {
|
||||
const payload = {
|
||||
conversationId: 'dm-group-test',
|
||||
messages: [],
|
||||
participants: [alice, bob],
|
||||
sender: alice,
|
||||
syncedAt: 30
|
||||
};
|
||||
|
||||
expect(directMessageSyncIncludesUser(payload, 'alice')).toBe(true);
|
||||
expect(directMessageSyncIncludesUser(payload, 'charlie')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function createMessage(
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
advanceDirectMessageStatus,
|
||||
createDirectConversation,
|
||||
createGroupConversation,
|
||||
directMessageConversationIncludesUser,
|
||||
directMessageEventIncludesUser,
|
||||
directMessageSyncIncludesUser,
|
||||
getDirectConversationId,
|
||||
isGroupDirectConversation,
|
||||
updateMessageStatusInConversation,
|
||||
@@ -464,6 +467,11 @@ export class DirectMessageService {
|
||||
private async handleIncomingMessage(payload: DirectMessageEventPayload): Promise<void> {
|
||||
const ownerId = this.getCurrentUserIdOrThrow();
|
||||
const currentUser = this.requireCurrentUser();
|
||||
|
||||
if (!directMessageEventIncludesUser(payload, ownerId) || payload.sender.userId === ownerId || payload.message.senderId === ownerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentParticipant = toDirectMessageParticipant(currentUser);
|
||||
const sender = payload.sender;
|
||||
const conversationId = payload.message.conversationId
|
||||
@@ -528,7 +536,11 @@ export class DirectMessageService {
|
||||
|
||||
private async handleIncomingMutation(payload: DirectMessageMutationEventPayload): Promise<void> {
|
||||
const ownerId = this.getCurrentUserIdOrThrow();
|
||||
const conversation = await this.requireConversation(ownerId, payload.conversationId);
|
||||
const conversation = await this.findConversation(ownerId, payload.conversationId);
|
||||
|
||||
if (!conversation || !directMessageConversationIncludesUser(conversation, ownerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.persistConversation(ownerId, this.applyMutation(conversation, payload));
|
||||
}
|
||||
@@ -540,6 +552,14 @@ export class DirectMessageService {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = this.conversationsSignal().find((entry) => entry.id === payload.conversationId);
|
||||
|
||||
if (!conversation
|
||||
|| !directMessageConversationIncludesUser(conversation, currentUserId)
|
||||
|| !directMessageConversationIncludesUser(conversation, payload.sender.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.isTyping) {
|
||||
this.typingEntriesSignal.update((entries) => entries.filter((entry) =>
|
||||
!(entry.conversationId === payload.conversationId && entry.userId === payload.sender.userId)
|
||||
@@ -566,10 +586,12 @@ export class DirectMessageService {
|
||||
private async handleIncomingSyncRequest(payload: DirectMessageSyncRequestEventPayload): Promise<void> {
|
||||
const ownerId = this.getCurrentUserIdOrThrow();
|
||||
const currentUser = this.requireCurrentUser();
|
||||
const conversation = this.conversationsSignal().find((entry) => entry.id === payload.conversationId)
|
||||
?? await this.repository.getConversation(ownerId, payload.conversationId);
|
||||
const conversation = await this.findConversation(ownerId, payload.conversationId);
|
||||
|
||||
if (!conversation || payload.sender.userId === ownerId) {
|
||||
if (!conversation
|
||||
|| payload.sender.userId === ownerId
|
||||
|| !directMessageConversationIncludesUser(conversation, ownerId)
|
||||
|| !directMessageConversationIncludesUser(conversation, payload.sender.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -596,6 +618,10 @@ export class DirectMessageService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!directMessageSyncIncludesUser(payload, ownerId) || !directMessageSyncIncludesUser(payload, payload.sender.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingConversation = this.conversationsSignal().find((conversation) => conversation.id === payload.conversationId)
|
||||
?? await this.repository.getConversation(ownerId, payload.conversationId)
|
||||
?? (payload.conversationKind === 'group' || payload.participants.length > 2
|
||||
@@ -863,10 +889,7 @@ export class DirectMessageService {
|
||||
}
|
||||
|
||||
private async requireConversation(ownerId: string, conversationId: string): Promise<DirectMessageConversation> {
|
||||
await this.loadForOwner(ownerId);
|
||||
|
||||
const conversation = this.conversationsSignal().find((entry) => entry.id === conversationId)
|
||||
?? await this.repository.getConversation(ownerId, conversationId);
|
||||
const conversation = await this.findConversation(ownerId, conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error('Direct message conversation not found.');
|
||||
@@ -875,6 +898,13 @@ export class DirectMessageService {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private async findConversation(ownerId: string, conversationId: string): Promise<DirectMessageConversation | null> {
|
||||
await this.loadForOwner(ownerId);
|
||||
|
||||
return this.conversationsSignal().find((entry) => entry.id === conversationId)
|
||||
?? await this.repository.getConversation(ownerId, conversationId);
|
||||
}
|
||||
|
||||
private requireCurrentUser(): User {
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type {
|
||||
DirectMessage,
|
||||
DirectMessageConversation,
|
||||
DirectMessageEventPayload,
|
||||
DirectMessageParticipant,
|
||||
DirectMessageSyncEventPayload,
|
||||
DirectMessageStatus
|
||||
} from '../models/direct-message.model';
|
||||
|
||||
@@ -74,6 +76,27 @@ export function isGroupDirectConversation(conversation: DirectMessageConversatio
|
||||
return conversation.kind === 'group' || conversation.participants.length > 2;
|
||||
}
|
||||
|
||||
export function directMessageConversationIncludesUser(
|
||||
conversation: Pick<DirectMessageConversation, 'participantProfiles' | 'participants'>,
|
||||
userId: string
|
||||
): boolean {
|
||||
return conversation.participants.includes(userId) || !!conversation.participantProfiles[userId];
|
||||
}
|
||||
|
||||
export function directMessageEventIncludesUser(
|
||||
payload: DirectMessageEventPayload,
|
||||
userId: string
|
||||
): boolean {
|
||||
return collectDirectMessageEventParticipantIds(payload).has(userId);
|
||||
}
|
||||
|
||||
export function directMessageSyncIncludesUser(
|
||||
payload: DirectMessageSyncEventPayload,
|
||||
userId: string
|
||||
): boolean {
|
||||
return payload.participants.some((participant) => participant.userId === userId);
|
||||
}
|
||||
|
||||
export function upsertDirectMessage(
|
||||
conversation: DirectMessageConversation,
|
||||
message: DirectMessage,
|
||||
@@ -129,6 +152,30 @@ function uniqueDirectMessageParticipants(participants: DirectMessageParticipant[
|
||||
});
|
||||
}
|
||||
|
||||
function collectDirectMessageEventParticipantIds(payload: DirectMessageEventPayload): Set<string> {
|
||||
const participantIds = new Set<string>();
|
||||
|
||||
if (payload.message.senderId) {
|
||||
participantIds.add(payload.message.senderId);
|
||||
}
|
||||
|
||||
if (payload.message.recipientId) {
|
||||
participantIds.add(payload.message.recipientId);
|
||||
}
|
||||
|
||||
for (const recipientId of payload.message.recipientIds ?? []) {
|
||||
participantIds.add(recipientId);
|
||||
}
|
||||
|
||||
for (const participant of payload.participants ?? []) {
|
||||
if (participant.userId) {
|
||||
participantIds.add(participant.userId);
|
||||
}
|
||||
}
|
||||
|
||||
return participantIds;
|
||||
}
|
||||
|
||||
function buildGroupConversationTitle(participants: DirectMessageParticipant[]): string {
|
||||
const names = participants.map((participant) => participant.displayName || participant.username || participant.userId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user