convokit_flutter 0.10.0
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_changednotifications. 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 neithereditMessagenor 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 explicitnull(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.Messagekeeps 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 passingnullclears 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
replyToMessageIdtoConvoKit.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 codeMESSAGE_NOT_FOUND(ConvoKitNotFoundException) and a blank id or one over 64 characters is a 400INVALID_ARGUMENT. Retrying with the sameclientMessageIdreturns 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=) returningList<ReplyPreview>(id,conversationId,senderId— the same valueMessage.senderIdcarries, 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 distinctreplyToMessageIdvalues 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 throwsArgumentErrorbefore any request. - Add
ConvoKit.getMessageContext(conversationId, {messageId, olderCursor, newerCursor, limit})(GET /api/v1/conversations/:id/context) returning aMessageContextPage(messages,olderCursor,newerCursor). This is the random-access companion togetMessages, which can only walk older from the newest page: withmessageIdthe window is centred on that message and is alwaysmin(limit, messages in the room)long, never short merely because the target sits near an end. Exactly one ofmessageId,olderCursorandnewerCursormust be given andlimitmust be 1..100 (default 30); anything else throwsArgumentErrorbefore any request.newerCursor == nullonly means the window touched the newest message at query time, so return to the live tail with a normalgetMessagesload 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(ConvoKitNotFoundExceptionwithout 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 nocode. 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 updatedMessage, andConvoKit.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 administrativePATCH/DELETE /api/v1/messages/:idyour server calls with the client secret. The edit body always carries both keys,{"text": ..., "revision": n}, with a null text serialised as JSONnullso a caption can be cleared on a message that has attachments (a text-only message cannot be emptied: a plain 400ConvoKitValidationExceptionwithout acode; only a malformedtextorrevisionbody is a 400 with codeINVALID_ARGUMENT); attachments are never changed by an edit.revisionmust be an integer in 0..2147483647 and throwsArgumentErrorbefore any request otherwise. - Add
Message.revision(trailing named constructor parameter, default 0) and the derivedMessage.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.updatedAtis never the edited signal. The parser treats a missing or nullrevision(a 0.7 backend) as 0 and fails on a present but malformed value like every other model field.Messagekeeps identity equality; existing literals are unchanged. - Conflicts: pass the
revisionof the row the user was shown. When it no longer matches, the backend answers 409 with codeREVISION_CONFLICT(ConvoKitValidationExceptionwithstatusCode == 409); reload the row withgetMessageand 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 codeMESSAGE_NOT_FOUND(ConvoKitNotFoundException) for both calls; another member's message is a 403. Consumers that merge rows for one id should let the higherrevisionwin and fall back toupdatedAt ?? createdAtonly for equal revisions. - Deleting is unconditional and cannot be undone: other devices receive
onMessageDeletedand 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/ownroutes 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 aConversationPrivateState(conversationId,unreadMarkedAt,privateStateVersion), andConvoKit.clearConversationUnread(conversationId, {int? ifVersion})(DELETE .../unread[?privateStateVersion=]) returning aClearUnreadResult(the same state pluscleared). The marker is private to the caller's own membership; every mark bumps the version, also a repeat mark. A clear whoseifVersionno longer matches, or with nothing marked, is a 200 withcleared == false, not an error.ifVersionmust be an integer in 0..2147483647 and throwsArgumentErrorbefore any request otherwise. - Add
privateStateVersiontoConvoKit.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 throwArgumentError. - Add
ConversationMembership(role,lastReadAt,readPosition,unreadMarkedAt,privateStateVersion; value equality) and the nullableConversation.membership, parsed from the self-onlymembershipsibling ofGET /api/v1/conversations/:id.Conversation.fromJsonaccepts an optional namedmembership; list responses and existing literals are unchanged and the field is null against a 0.6 backend. Capturemembership?.privateStateVersionwhen a room opens and send it with every acknowledgement of that open. - Add
isUnread,unreadMarkedAtandprivateStateVersiontoInboxSummaryandInboxEntry(trailing named constructor parameters;isUnreadis derived asunreadCount > 0 || unreadCountCapped || unreadMarkedAt != nullwhen omitted,unreadMarkedAtdefaults to null andprivateStateVersionto 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.unreadCountis never inflated by a marker: render a numberless dot whenisUnreadis true with a zero count. Value equality,hashCodeandtoStringcover the new fields. onInboxActivityalso fires after the caller's own marker changes (a mark, a clear, or an acknowledgement that clears it), so other devices refetchlistInbox. No new realtime event. Requires the coordinated 0.7 backend release; an older backend answers the/unreadroutes with 404.
0.6.0 #
- Add
ConvoKit.listInbox({limit = 30, cursor, archived = false})returning anInboxPagefromGET /api/v1/inbox:entriesordered by activity time then conversation id (both descending) and an opaquenextCursor(null on the last page).limitmust be an integer in 1..100 and throwsArgumentErrorbefore any request otherwise;cursoris omitted from the query when null. A rejected cursor surfaces asConvoKitValidationExceptionwithcode == 'INVALID_CURSOR'.getConversations(creation order, offset paging) is unchanged. - Add
InboxEntry(conversationplussummary) andInboxSummary(latestMessagein thegetMessagesrow shape or null,unreadCount,unreadCountCapped, the caller'sreadPositionandlastReadAt, andactivityAt), parsed from camelCase or snake_case with value equality. The entry'sconversation.participantsis bounded to ten members and the preview'smediato four items. Unread counts follow thereadThroughrule (messages from others after the caller's position; own messages never count) over a 1,000-message window, withunreadCountCappedmarking 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. UnlikeonInboxChangedit is never synthesised on a verified join or rejoin; keep listening toonInboxChangedfor structural changes and reconnect reconciliation. Requires the coordinated backend release; an older backend answerslistInboxwith 404 and never broadcasts the event.
0.5.0 #
- Add
throughMessageIdtomarkConversationReadso a read acknowledges a concrete message. The backend stores that message's(createdAt, id)cursor as a monotonicReadPosition, separate from thelastReadAtacknowledgement 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, UTCcreatedAt,covers(message), value equality),Participant.readPositionandReadEvent.readPosition(both nullable, parsed from camelCase or snake_case), and the top-levelreadThrough(message, readPosition:, lastReadAt:)rule shared by every ConvoKit client: position when present, otherwiselastReadAt >= createdAt. - Add optional
ConvoKitException.codeparsed from the error body, for exampleMESSAGE_NOT_FOUNDon a targeted read whose message is gone or foreign.ConvoKitSessionException.codeis 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
clientMessageIdthrough REST, history and live rows; exposecreateClientMessageId()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.updatedAtrevisions for consistent reconciliation of REST responses and Realtime edits. Creation timestamps and device-local display behavior are unchanged. -
Add value-based
beforeCreatedAt/beforeIdhistory 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.deletewith the separateonMessageDeleted()/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.appendpoint by default. - Keep
backendUrlas an optional override for local testing and self-hosting.
0.0.3 #
- Breaking:
RealtimeService.onMessage()now returnsStream<MessageEvent>instead ofStream<Message>, and deliversUPDATE/DELETEevents onMessagein addition toINSERT. Switch onMessageEvent.typeand readMessageEvent.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:
ConvoKitExceptionis now a base type withConvoKitAuthException,ConvoKitNotFoundException,ConvoKitValidationException,ConvoKitServerException, andConvoKitNetworkExceptionsubtypes. - Added
ConvoKit.getUser()/ConvoKit.getUsers()and a newAppUsermodel for looking up app user profiles, includinglastSeenAt. - 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.