Skip to content
TildeMailv0.2.1

Chat contracts

TildeMail Core defines protocol-neutral contracts for chat backends. Each IChatBackend represents exactly one account. The contracts provide shared data and operation requirements; they do not connect to a service or implement a chat runtime.

IChatBackend.AccountId is the stable, nonblank identifier for the account. A ChatConversationId combines that account ID with an opaque protocol Address. Both strings must be nonblank and are preserved exactly. Equality is ordinal, so the same address under different accounts identifies different conversations. Core does not normalize protocol addresses; adapters choose their protocol-specific representation.

ChatConversation describes an identified conversation with a nonblank display Name and a Type of Direct or Room.

ChatMessage is an immutable observation associated with a ChatConversation. It carries a nonblank opaque Sender, nonnull Text, a DateTimeOffset Timestamp, an optional protocol Id, and an IsOutgoing flag. Text, identifiers, and timestamp are preserved as supplied; text may be empty or whitespace, and the timestamp retains its offset and precision. An unavailable message ID is null; supplied IDs must be nonblank and are never fabricated. IDs are not guaranteed unique and are not, by themselves, message identity or a deduplication key. Timestamps need not be globally ordered. A message does not imply delivery, read status, or encryption.

The interface exposes the account ID, a current State (Disconnected, Connecting, Connected, or Disconnecting), and hot, non-replaying StateChanges and Messages observables. Subscribe before connecting to receive events. Notifications are serialized within each stream, have no cross-stream ordering guarantee, and may arrive off the UI thread; consumers must marshal them if needed. Messages reports transport observations, including own-message echoes when supported. Sending does not automatically create a local echo.

Implementations of the interface are required to follow these lifecycle and send guarantees:

  • Construction does not connect or start background work; the initial state is Disconnected.
  • Callers serialize lifecycle operations and coordinate sends with disconnect and disposal; concurrent calls have no guaranteed ordering. Connecting when already connected and disconnecting when already disconnected are no-ops. A failed or cancelled connection attempt leaves the backend disconnected. Cancellation tokens apply to their individual operations, not to the established connection after ConnectAsync completes.
  • Disconnecting and reconnecting keep both observables open. Recoverable connection failures do not terminate a stream with OnError; state changes use StateChanges, while operation failures are reported by the returned tasks.
  • SendTextAsync requires a connected backend, nonblank text, and a conversation whose account ID matches AccountId using ordinal equality. Completion means the transport accepted the text, not that it was delivered or read. Cancellation or failure does not prove the remote side did not receive it, so callers must not retry blindly.
  • Disposal releases resources and completes both streams. It is safe to repeat; later connection and send operations fail with ObjectDisposedException.

These are requirements for future adapters. The Core implementation contains the interface, immutable models, and enums; it includes no IRC or XMPP SDK, network connection, runtime service, factory or registry, plugin registration API, account configuration, or persistence.

The TUI has a shared immutable ConversationListItem presentation snapshot for mail threads and chat conversations. Its source-qualified ID keeps source, account, scope, and local ID as separate opaque parts. Optional preview, activity time, and unread count remain unknown (null) when unavailable; a known unread count of zero is distinct. Display title and preview are cleaned for terminal output without changing the source data.

MailConversationListAdapter.Create projects the loaded members of an already grouped mail thread. It uses the newest message as the title and activity source, leaves preview unknown, and counts unread loaded messages only. Its ID uses the account, exact folder, and smallest loaded member UID. This is a view-scoped key that can change when older messages load; it is not a persistent thread identity.

ChatConversationListAdapter.Create projects a chat descriptor and optional last-message observation. The caller supplies the observation and any unread count; absent values stay unknown. The adapter does not infer read state from message direction or provide chat history.

The existing mail conversation list consumes this snapshot for grouped thread heads, including their title, activity date, and aggregate unread highlighting. Expanded child rows retain their own message subject and date, and mail-specific navigation and actions remain in place. This adds no chat connector, chat screen, or chat operations.