diff --git a/packages/iios-messaging-ui/src/types.test.ts b/packages/iios-messaging-ui/src/types.test.ts new file mode 100644 index 0000000..da334a2 --- /dev/null +++ b/packages/iios-messaging-ui/src/types.test.ts @@ -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); + }); +}); diff --git a/packages/iios-messaging-ui/src/types.ts b/packages/iios-messaging-ui/src/types.ts new file mode 100644 index 0000000..ae99f9b --- /dev/null +++ b/packages/iios-messaging-ui/src/types.ts @@ -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; +}