feat: add messaging-ui domain types

This commit is contained in:
2026-07-18 00:41:05 +05:30
parent 167171a682
commit 0c9b27684b
2 changed files with 92 additions and 0 deletions
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { isOwnMessage } from './types';
import type { Message } from './types';
const base: Message = {
id: 'm1',
actorId: 'actor_a',
text: 'hello',
at: '2026-07-17T10:00:00.000Z',
};
describe('isOwnMessage', () => {
it('is true when the message actor matches the current actor', () => {
expect(isOwnMessage(base, 'actor_a')).toBe(true);
});
it('is false when the actors differ', () => {
expect(isOwnMessage(base, 'actor_b')).toBe(false);
});
it('is false when the current actor is unknown', () => {
expect(isOwnMessage(base, null)).toBe(false);
});
it('is false when the message has no actor', () => {
expect(isOwnMessage({ ...base, actorId: null }, 'actor_a')).toBe(false);
});
});
+64
View File
@@ -0,0 +1,64 @@
// Domain types for the messaging UI. Zero imports on purpose: this file must never
// reach for a transport package. Adapters map their own DTOs onto these.
export type Membership = 'dm' | 'group';
export interface Person {
id: string;
name: string;
kind: 'staff' | 'customer';
}
export interface Conversation {
threadId: string;
title: string;
subject: string | null;
membership: Membership | null;
participants: string[];
unread: number;
lastMessage?: string;
lastAt?: string;
}
export interface Reaction {
emoji: string;
count: number;
mine: boolean;
}
export interface Attachment {
url: string;
mime: string;
name: string;
}
export interface Message {
id: string;
actorId: string | null;
text: string;
at: string;
parentInteractionId?: string | null;
reactions?: Reaction[];
attachment?: Attachment;
/** True only when the transport is optimistic-local and not yet acknowledged. */
pending?: boolean;
}
export interface SendOpts {
parentInteractionId?: string;
attachment?: Attachment;
}
export type MessageEvent =
| { kind: 'message'; message: Message }
| { kind: 'typing'; userId: string }
| { kind: 'receipt'; messageId: string; actorId: string }
| { kind: 'reaction'; messageId: string; reactions: Reaction[] };
export type Unsubscribe = () => void;
/** Ownership is a pure function of explicit identity — never inferred from history.
* See the spec: inferring it is the bug this SDK exists partly to kill. */
export function isOwnMessage(message: Message, currentActorId: string | null): boolean {
return currentActorId !== null && message.actorId !== null && message.actorId === currentActorId;
}