convokit_flutter 0.10.0 copy "convokit_flutter: ^0.10.0" to clipboard
convokit_flutter: ^0.10.0 copied to clipboard

Flutter SDK for ConvoKit - real-time messaging integration.

0.10.0 #

  • Add exact-emoji reaction add/remove, batched conversation summaries, cursor-paged reactor lists, and private reaction_changed notifications. Duplicate mutations are idempotent; callers refetch summaries after notifications or reconnect.

0.9.0 #

  • Add Message.replyToMessageId (trailing named constructor parameter, default null): the id of the message this one quotes, or null when it is not a reply. The reference is write-once — the backend sets it at send and neither editMessage nor an administrative edit can change it — and it survives the quoted message being edited or deleted, so a dangling id means the original is gone, not that the row is broken. A missing key (a 0.8 backend) and an explicit null (a 0.9 backend, which sends the key on every row) parse to the same single state; a present but malformed value fails like every other model field. REST rows, inbox previews (InboxSummary.latestMessage) and Realtime INSERT/UPDATE row images all carry it. Message keeps identity equality; existing literals are unchanged.
  • Add Message.copyWith(...), the first copy helper on the model. Every nullable field (clientMessageId, text, updatedAt, replyToMessageId) takes a private sentinel as its default, so omitting it keeps the current value while passing null clears it. Use it instead of rebuilding a row field by field: a hand-written literal silently drops any field it forgets, which is how a reply loses its reference — and with it the quoted block and the jump affordance — when a row is re-wrapped for a pending send, a media hydration or a bare Realtime row image.
  • Add replyToMessageId to ConvoKit.sendMessage. The key is omitted entirely when null, so a plain send is byte-identical to one from a 0.8 SDK. The target must be a surviving message of the same conversation: a missing, deleted or foreign id is a 404 with code MESSAGE_NOT_FOUND (ConvoKitNotFoundException) and a blank id or one over 64 characters is a 400 INVALID_ARGUMENT. Retrying with the same clientMessageId returns the stored row unchanged even when the quoted message was deleted in the meantime; retrying it with a different reply target is a 409.
  • Add ConvoKit.getReplyPreviews(conversationId, messageIds: [...]) (GET /api/v1/conversations/:id/reply-previews?ids=) returning List<ReplyPreview> (id, conversationId, senderId — the same value Message.senderId carries, named for the wire so the model is identical on every ConvoKit SDK — text, textTruncated, createdAt, revision, mediaCount; value equality). Call it once per rendered page with the distinct replyToMessageId values on screen, never once per row: the ids are trimmed, de-duplicated preserving first-seen order and split into requests of at most 50 distinct ids, merged in chunk order. Chunking is invisible and all-or-nothing, so an unsent chunk's ids can never be mistaken for deleted messages. Ids that do not exist, were deleted, or belong to another room are simply absent from a returned result — that is the only deletion signal and never an error. An empty list, a blank id or an id over 64 characters throws ArgumentError before any request.
  • Add ConvoKit.getMessageContext(conversationId, {messageId, olderCursor, newerCursor, limit}) (GET /api/v1/conversations/:id/context) returning a MessageContextPage (messages, olderCursor, newerCursor). This is the random-access companion to getMessages, which can only walk older from the newest page: with messageId the window is centred on that message and is always min(limit, messages in the room) long, never short merely because the target sits near an end. Exactly one of messageId, olderCursor and newerCursor must be given and limit must be 1..100 (default 30); anything else throws ArgumentError before any request. newerCursor == null only means the window touched the newest message at query time, so return to the live tail with a normal getMessages load rather than treating a jumped window as live.
  • Both new reads are conversation-scoped and check active membership before any message id is read: an outsider, a departed member or a room in another app is a 404 Conversation not found (ConvoKitNotFoundException without a code). A read-only role may read them; neither route ever answers 403. Requires the coordinated 0.9 backend release: an older backend answers both routes with an unmatched-route 404 that carries no code. That is a missing backend, not missing messages — hide the jump affordance and leave reply previews unresolved instead of rendering "original message unavailable".

0.8.0 #

  • Add ConvoKit.editMessage(messageId, {required String? text, required int revision}) (PATCH /api/v1/messages/:id/own) returning the updated Message, and ConvoKit.deleteMessage(messageId) (DELETE /api/v1/messages/:id/own) returning void. Both are author-tier calls for the current user's own messages, separate from the administrative PATCH/DELETE /api/v1/messages/:id your server calls with the client secret. The edit body always carries both keys, {"text": ..., "revision": n}, with a null text serialised as JSON null so a caption can be cleared on a message that has attachments (a text-only message cannot be emptied: a plain 400 ConvoKitValidationException without a code; only a malformed text or revision body is a 400 with code INVALID_ARGUMENT); attachments are never changed by an edit. revision must be an integer in 0..2147483647 and throws ArgumentError before any request otherwise.
  • Add Message.revision (trailing named constructor parameter, default 0) and the derived Message.isEdited (revision > 0). The backend sets 0 at creation and increments by one on every content edit, author or administrative (media-only administrative edits included); the value is carried by REST rows, inbox previews (InboxSummary.latestMessage) and Realtime UPDATE row images. updatedAt is never the edited signal. The parser treats a missing or null revision (a 0.7 backend) as 0 and fails on a present but malformed value like every other model field. Message keeps identity equality; existing literals are unchanged.
  • Conflicts: pass the revision of the row the user was shown. When it no longer matches, the backend answers 409 with code REVISION_CONFLICT (ConvoKitValidationException with statusCode == 409); reload the row with getMessage and retry with its revision. A message that does not exist, was deleted, or belongs to a room the caller is not an active member of answers 404 with code MESSAGE_NOT_FOUND (ConvoKitNotFoundException) for both calls; another member's message is a 403. Consumers that merge rows for one id should let the higher revision win and fall back to updatedAt ?? createdAt only for equal revisions.
  • Deleting is unconditional and cannot be undone: other devices receive onMessageDeleted and the inbox preview moves to the previous surviving message. Files already received or downloaded cannot be retracted; stored files are reclaimed by the existing user or app deletion cleanup, not by this call. Requires the coordinated 0.8 backend release: an older backend answers the /own routes with an unmatched-route 404 without a code, which must not be treated as "message gone".

0.7.0 #

  • Add ConvoKit.markConversationUnread(conversationId) (POST /api/v1/conversations/:id/unread) returning a ConversationPrivateState (conversationId, unreadMarkedAt, privateStateVersion), and ConvoKit.clearConversationUnread(conversationId, {int? ifVersion}) (DELETE .../unread[?privateStateVersion=]) returning a ClearUnreadResult (the same state plus cleared). The marker is private to the caller's own membership; every mark bumps the version, also a repeat mark. A clear whose ifVersion no longer matches, or with nothing marked, is a 200 with cleared == false, not an error. ifVersion must be an integer in 0..2147483647 and throws ArgumentError before any request otherwise.
  • Add privateStateVersion to ConvoKit.markConversationRead, serialised only when given: the acknowledgement clears the caller's marker only when the sent version equals the current one; the position rule and the exact-204 contract are unchanged, and the legacy body never clears. A version-only call clears the marker in an empty room; in a non-empty one it also acknowledges through the newest stored message, like the legacy form. Values outside 0..2147483647 throw ArgumentError.
  • Add ConversationMembership (role, lastReadAt, readPosition, unreadMarkedAt, privateStateVersion; value equality) and the nullable Conversation.membership, parsed from the self-only membership sibling of GET /api/v1/conversations/:id. Conversation.fromJson accepts an optional named membership; list responses and existing literals are unchanged and the field is null against a 0.6 backend. Capture membership?.privateStateVersion when a room opens and send it with every acknowledgement of that open.
  • Add isUnread, unreadMarkedAt and privateStateVersion to InboxSummary and InboxEntry (trailing named constructor parameters; isUnread is derived as unreadCount > 0 || unreadCountCapped || unreadMarkedAt != null when omitted, unreadMarkedAt defaults to null and privateStateVersion to 0, so existing literals keep their badges). The parser applies the same defaults for a 0.6 backend and fails on a present but malformed value. unreadCount is never inflated by a marker: render a numberless dot when isUnread is true with a zero count. Value equality, hashCode and toString cover the new fields.
  • onInboxActivity also fires after the caller's own marker changes (a mark, a clear, or an acknowledgement that clears it), so other devices refetch listInbox. No new realtime event. Requires the coordinated 0.7 backend release; an older backend answers the /unread routes with 404.

0.6.0 #

  • Add ConvoKit.listInbox({limit = 30, cursor, archived = false}) returning an InboxPage from GET /api/v1/inbox: entries ordered by activity time then conversation id (both descending) and an opaque nextCursor (null on the last page). limit must be an integer in 1..100 and throws ArgumentError before any request otherwise; cursor is omitted from the query when null. A rejected cursor surfaces as ConvoKitValidationException with code == 'INVALID_CURSOR'. getConversations (creation order, offset paging) is unchanged.
  • Add InboxEntry (conversation plus summary) and InboxSummary (latestMessage in the getMessages row shape or null, unreadCount, unreadCountCapped, the caller's readPosition and lastReadAt, and activityAt), parsed from camelCase or snake_case with value equality. The entry's conversation.participants is bounded to ten members and the preview's media to four items. Unread counts follow the readThrough rule (messages from others after the caller's position; own messages never count) over a 1,000-message window, with unreadCountCapped marking a lower bound.
  • Add ConvoKit.realtime.onInboxActivity(appId): an empty private app-hub signal after a message insert or edit and after a read-position advance. Unlike onInboxChanged it is never synthesised on a verified join or rejoin; keep listening to onInboxChanged for structural changes and reconnect reconciliation. Requires the coordinated backend release; an older backend answers listInbox with 404 and never broadcasts the event.

0.5.0 #

  • Add throughMessageId to markConversationRead so a read acknowledges a concrete message. The backend stores that message's (createdAt, id) cursor as a monotonic ReadPosition, separate from the lastReadAt acknowledgement time. The request body is now { "throughMessageId": ... } or {}, the conversation id path segment is percent-encoded, and the exact-204 success check is unchanged. Requires the coordinated backend release; an older backend ignores the target.
  • Add ReadPosition (messageId, UTC createdAt, covers(message), value equality), Participant.readPosition and ReadEvent.readPosition (both nullable, parsed from camelCase or snake_case), and the top-level readThrough(message, readPosition:, lastReadAt:) rule shared by every ConvoKit client: position when present, otherwise lastReadAt >= createdAt.
  • Add optional ConvoKitException.code parsed from the error body, for example MESSAGE_NOT_FOUND on a targeted read whose message is gone or foreign. ConvoKitSessionException.code is unchanged.
  • Mixed fleet: precise receipts need sender and reader on 0.5. 0.4 readers keep timestamp semantics and still parse the additive payloads.

0.4.0 #

  • Add private onInboxChanged(appId) signals on mutations and verified subscription/reconnect, sharing the app presence channel.
  • Replace the unimplemented onConversationUpdate() placeholder with that explicit inbox API. This requires the matching backend/UI release.
  • Generate and preserve clientMessageId through REST, history and live rows; expose createClientMessageId() for custom optimistic UIs and accept the backend's HTTP 200 result for an identical retry.

0.3.1 #

  • Include the minimum-Flutter test lint correction. The 0.3.0 publishing run stopped at validation; its tag is retained without moving or replacing it.
  • Automatically discover rotating private Broadcast topics and rejoin remaining room/app listeners after membership removal or reconnect. Requires the matching backend cutover; no retired-topic fallback or customer Supabase configuration.

  • Preserve optional server Message.updatedAt revisions for consistent reconciliation of REST responses and Realtime edits. Creation timestamps and device-local display behavior are unchanged.

  • Add value-based beforeCreatedAt / beforeId history cursors so pagination stays stable when rows are inserted or the previous page's last row is deleted.

  • Own a dedicated Supabase client per connected user. Do not initialize or modify the host app's global Supabase instance.

  • Coordinate proactive and 401-triggered renewal of both user and Realtime tokens, with single-flight refresh, bounded backoff and independent expiry.

  • Reject pending work on logout/reconfiguration/user switching; bind all upload stages to their initiating session and never retry storage PUTs implicitly.

  • Share private room channels across typing/read listeners and release channels when their final listener cancels. Refresh credentials after delayed joins.

  • Serialize channel cleanup/re-entry and recover unexpected provider channel closes without losing listeners; discard paused events after session retirement.

  • Keep Realtime authentication explicitly owned by ConvoKit, avoiding a provider async-callback bug that resends pending joins with empty references. Preserve the Supabase HTTP user-token callback and never substitute a project key.

  • Expose sanitized session/Realtime errors and per-topic connection events for reconnect reconciliation. HTTP 403/5xx/network failures do not replay writes.

  • Breaking: replace MessageChangeType.delete with the separate onMessageDeleted() / MessageDeletedEvent(id, conversationId) contract. Receive only the backend's private ID-only deletion broadcast, validate its room, and share typing/read channel lifecycle, errors and session disposal. Never subscribe to raw Postgres DELETE or fabricate old Message records. Explicit message deletion requires the coordinated backend release; missed events need REST reconciliation; cascade inbox discovery requires refresh/reopen.

  • Require supabase_flutter >=2.14.0 <3.0.0, Flutter >=3.19, and Dart >=3.3. The former dependency floor admitted releases missing the SDK's auth APIs.

  • Validate minimum and current Flutter/dependency resolutions in CI and before publishing, including loopback tests using the actual Supabase WebSocket client. Live security smoke tests still require explicit staging fixtures.

0.2.0 #

  • Use the Supabase publishable key discovered from the ConvoKit token service.
  • Authenticate Realtime before joining private message, typing, read-receipt, and presence channels.
  • Keep connectUser, token-provider, and managed API endpoint interfaces unchanged.

0.0.4 #

  • Use the managed https://api.convokit.app endpoint by default.
  • Keep backendUrl as an optional override for local testing and self-hosting.

0.0.3 #

  • Breaking: RealtimeService.onMessage() now returns Stream<MessageEvent> instead of Stream<Message>, and delivers UPDATE/DELETE events on Message in addition to INSERT. Switch on MessageEvent.type and read MessageEvent.message.
  • Added automatic session keep-alive: the SDK refreshes the session proactively ahead of token expiry, and reactively (refresh-and-retry once) on any REST call that comes back 401.
  • Added a typed error hierarchy: ConvoKitException is now a base type with ConvoKitAuthException, ConvoKitNotFoundException, ConvoKitValidationException, ConvoKitServerException, and ConvoKitNetworkException subtypes.
  • Added ConvoKit.getUser() / ConvoKit.getUsers() and a new AppUser model for looking up app user profiles, including lastSeenAt.
  • Added ConvoKit.getMessage() to fetch a single message by id.

0.0.2 #

  • Finalize R2 uploads with the backend before returning media URLs.
  • Validate signed-upload object keys and surface upload-completion failures.

0.0.1 #

  • Initial release of the ConvoKit Flutter SDK.
  • Added SDK configuration, user connection, and token exchange helpers.
  • Added conversation, message, realtime, presence, read receipt, typing, and media upload APIs.
0
likes
150
points
682
downloads

Documentation

API reference

Publisher

verified publisherconvokit.app

Weekly Downloads

Flutter SDK for ConvoKit - real-time messaging integration.

Repository (GitHub)

License

Apache-2.0 (license)

Dependencies

flutter, http, supabase_flutter

More

Packages that depend on convokit_flutter