Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/---bug-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Hello 👋 Thank you for submitting an issue!

🚨 Please read the following carefully before opening a new issue. Your issue may be closed if it doesn't provide the required pieces of information.

🚨 Before you start, please make sure your issue is understandable and reproducible. To make your issue readable make sure you use valid Markdown syntax. https://guides.github.com/features/mastering-markdown/
🚨 Before you start, please make sure your issue is understandable and reproducible. To make your issue readable make sure you use valid Markdown syntax. https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax

🚨 Please ensure you have also read and understand the contributing guide.
https://github.com/insideapp-srl/flutter_supabase_chat_core/blob/main/CONTRIBUTING.md
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Link preview
url: https://github.com/flyerhq/flutter_link_previewer/issues/new
url: https://github.com/flyerhq/flutter_chat_ui/issues/new/choose
about: Use this to report link preview issues.
- name: Chat UI
url: https://github.com/flyerhq/flutter_chat_ui/issues/new/choose
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
## [2.0.0-rc.2] - 2026-10-22
#### [@rickypid](https://github.com/rickypid)

Second release candidate of 2.0.0: message forwarding and a clean session
teardown on sign out. No database changes since rc.1.

### Added

* `SupabaseChatCore.forwardMessages`: forwards messages to another room as
new messages of the logged-in user, oldest first. Each copy stores its
`ChatForwardOrigin` (original author and date) in the metadata and drops
reply, reactions, pin and edited mark; chat attachments are copied into the
destination room's folder.
* `ChatForwardOrigin`: reads the origin of a forwarded message
(`ChatForwardOrigin.of(message)`).
* `SupabaseChatCore.closeSession()`: closes the own presence, room channels,
presence watches and the `roomsChanges`/`room`/`userOnlineStatus` streams.
Call it right before signing out, so private room channels leave cleanly
instead of reporting subscription errors.

### Changed

* Signing out or switching user closes the session automatically: the
`roomsChanges`, `room` and `userOnlineStatus` streams complete instead of
outliving the user, and `SupabaseChatRoomsController` clears its rooms
(the next `loadFirstPage` listens again for the new user).
* `ChatRoom` equality compares every field, not only the `id`: a refetched
room is no longer dropped as unchanged by lists and `ValueNotifier`s.
Match rooms by `id` to find the same room.
* The example app calls `closeSession()` before signing out.
* README and documentation site cover forwarding, signing out and the
session lifecycle.

### Fixed

* Auth state changes arriving in bursts (initial session, signed in, token
refreshed) could open the own presence channel twice.
* Late status callbacks of released room and presence channels changed the
state of the current ones.
* Documentation site: "Edit this page" links, and links to the Flyer Chat
2.x documentation instead of 1.x.

## [2.0.0-rc.1] - 2026-10-21
#### [@rickypid](https://github.com/rickypid)

Expand Down
2 changes: 1 addition & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ reference while porting.

```yaml
dependencies:
flutter_supabase_chat_core: ^2.0.0-rc.1
flutter_supabase_chat_core: ^2.0.0-rc.2
flutter_chat_core: ^2.9.0 # replaces flutter_chat_types
flutter_chat_ui: ^2.11.1 # if you use the UI (2.x API)
flyer_chat_text_message: ^2.6.0 # one package per message type you render
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Flyer Chat is a platform for creating in-app chat experiences using Flutter. Thi
- **Direct and group rooms**, created atomically by a database function. Direct rooms are de-duplicated per pair of users (and per metadata scope, see [Scoping](#scoping-workspaces-organizations)).
- **Group management**: per-room roles (`admin`, `moderator`), add/remove members, change roles, rename, leave, delete. A room left without members is deleted automatically.
- **Messages** of every `flutter_chat_core` type (text, image, file, audio with waveform, video, custom...), with replies, edits, reactions, pinned messages and search.
- **Forwarding**: forward messages to another room, keeping the original author and date; attachments are copied into the destination room.
- **Optimistic sending**: messages appear right away, are reconciled with the stored row and can be retried when they fail.
- **Read receipts and unread counters**: `markRoomAsRead` flips other members' messages to `seen` and resets the room's `unreadCount`.
- **Per-user preferences**: pin a room on top of your list, mute it for a while or for ever.
Expand All @@ -109,7 +110,7 @@ Flyer Chat is a platform for creating in-app chat experiences using Flutter. Thi

```yaml
dependencies:
flutter_supabase_chat_core: ^2.0.0-rc.1
flutter_supabase_chat_core: ^2.0.0-rc.2
supabase_flutter: ^2.17.2
uuid: ^4.5.1

Expand Down Expand Up @@ -376,12 +377,30 @@ Call `onTyping()` on every keystroke of the composer, e.g. with your own `TextEd
| Rooms | `rooms()`, `roomsChanges()` + `SupabaseChatCore.applyRoomChange()`, `room(id)` (stream), `getRoom()`, `createRoom()`, `createGroupRoom()`, `updateRoom()`, `deleteRoom()`, `markRoomAsRead()`, `pinRoom()`, `muteRoom()`, `unmuteRoom()` |
| Members | `addMembers()`, `removeMember()`, `leaveRoom()`, `setMemberRole()` |
| Users | `users(filter:, excludeLoggedUser:)`, `user()`, `updateUser()`, `uploadUserAvatar()`, `loggedUser`, `loggedUserId`, `refreshLoggedUser()` |
| Messages | `messages(roomId, before:, limit:)`, `message()`, `pinnedMessages()`, `searchMessages()`, `sendMessage()`, `updateMessage()`, `toggleReaction()`, `setMessagePinned()`, `deleteMessage()` |
| Messages | `messages(roomId, before:, limit:)`, `message()`, `pinnedMessages()`, `searchMessages()`, `sendMessage()`, `forwardMessages()`, `updateMessage()`, `toggleReaction()`, `setMessagePinned()`, `deleteMessage()` |
| Storage | `uploadAsset()`, `getAssetUrl()`, `httpSupabaseHeaders` (to load authenticated URLs) |
| Presence | `setPresenceStatus()`, `userOnlineStatus()` |
| Session | `closeSession()` (before signing out), `dispose()` |

`SupabaseChatCore.instance` is the singleton bound to `Supabase.instance.client`. Create your own instance with `SupabaseChatCore(client: ..., config: ...)` to use another client (e.g. several users in one process, as the integration tests do), and `dispose()` it when done. Controllers and widgets accept a `chatCore:` parameter for that.

### Forwarding messages

```dart
await SupabaseChatCore.instance.forwardMessages(fromRoom, selectedMessages, toRoomId);
```

The messages are sent to `toRoomId` as new messages of the logged-in user (a member of both rooms), oldest first. Each copy keeps content and metadata, drops reply, reactions, pin and edited mark, and stores where it comes from: read it with `ChatForwardOrigin.of(message)` (`authorId`, `authorName`, `createdAt`) to show a "Forwarded from ..." header. Forwarding a forwarded message keeps the first origin. The author's name is a snapshot, since the recipients may not be able to read the original author's profile. Attachments in the chat assets bucket are copied into the destination room's folder, so its members can read them; external URLs are kept as they are.

### Signing out

```dart
await SupabaseChatCore.instance.closeSession();
await Supabase.instance.client.auth.signOut();
```

`closeSession()` sets the user offline and closes everything opened for them: room channels, presence watches and the `roomsChanges()`, `room()` and `userOnlineStatus()` streams, which complete. Call it right **before** signing out: after the sign out Realtime is re-authorized with the anon key, which private room channels reject, so they would report subscription errors instead of leaving cleanly. Signing out (or switching user) without it still closes the session when the auth state change arrives, and `SupabaseChatRoomsController` clears its rooms until the next `loadFirstPage()`.

### Error handling

Every public method throws a `SupabaseChatCoreException` subtype, with the original error in `cause`:
Expand Down
2 changes: 2 additions & 0 deletions doc/docs/guides/supabase-realtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Realtime allows a single join per topic per connection, so a `SupabaseChatCore`

The Postgres Changes subscriptions behind `room(id)` and `roomsChanges()` use a unique topic per subscription, so several listeners of the same data can coexist. A failed or timed-out subscription is surfaced as a `SupabaseChatCoreRequestException` on the stream.

Everything above belongs to the signed-in user's session. `closeSession()`, a sign out or a switch to another user removes the room channels, the presence watches and the subscriptions behind `room(id)`, `roomsChanges()` and `userOnlineStatus()`, whose streams complete. Call `closeSession()` before signing out: once signed out, Realtime is re-authorized with the anon key and private room channels would fail instead of leaving cleanly. Auth state changes arrive in bursts (initial session, signed in, token refreshed) and are handled one at a time, so the own presence channel is opened once; status callbacks of channels already released are ignored.

## Typing indicator

The typing indicator used Realtime Presence up to 1.x. Supabase rate-limits Presence `track()` calls per client and drops the channel when the limit is exceeded, which a typing indicator updating every few hundred milliseconds hits within seconds ([issue #38](https://github.com/insideapp-srl/flutter_supabase_chat_core/issues/38)). Since 2.x it uses Broadcast events on the room channel, which are cheap and ephemeral.
Expand Down
29 changes: 29 additions & 0 deletions doc/docs/guides/supabase-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ final page = await SupabaseChatCore.instance.users(

`user(uid: id)` returns a single user. At the UI boundary, `ChatUser.toCoreUser()` converts it to the `flutter_chat_core` `User` expected by `Chat.resolveUser`.

### Signing out

Close the chat session right **before** signing out:

```dart
await SupabaseChatCore.instance.closeSession();
await Supabase.instance.client.auth.signOut();
```

`closeSession()` sets the user offline (see [Online status](#online-status)) and closes everything opened for them: the own presence, the room channels, the presence watches and the `roomsChanges()`, `room()` and `userOnlineStatus()` streams, which complete. The order matters: signing out re-authorizes Realtime with the anon key, which private room channels reject, so channels still open at that point report subscription errors instead of leaving cleanly.

Signing out or switching user without `closeSession()` still closes the session, as soon as the auth state change arrives: a listener never keeps receiving the data of a user that is no longer signed in. `SupabaseChatRoomsController` clears its rooms when nobody is signed in, and its next `loadFirstPage()` listens again for whoever is signed in then. Dispose the `SupabaseChatController`s of the previous user as usual.

## Rooms

### Creating rooms
Expand Down Expand Up @@ -107,6 +120,7 @@ Building your own list instead? `rooms(filter:, offset:, limit:)` returns a page

* `getRoom(id)` returns the room, or `null` if it doesn't exist or the user isn't a member.
* `room(id)` returns a stream that emits the room right away and on every change to it or its members, and completes when the room is deleted or the user leaves it.
* `ChatRoom` equality compares every field, so a room refetched after a change (a new message, the unread count, a rename) is not `==` to the copy you hold and isn't dropped as unchanged by lists or `ValueNotifier`s. Match rooms by `id` to find the same room.
* A `ChatRoom` exposes `users` (each with its per-room `role`), `member(id)`, `roleOf(id)`, `otherUser(myId)` for direct rooms, `lastMessage`, `unreadCount`, `lastReadAt`, `isPinned`/`pinnedAt` and `isMuted`/`mutedUntil` (`ChatRoom.mutedForever` when muted for ever).

### Managing rooms
Expand Down Expand Up @@ -227,6 +241,21 @@ With `autoMarkAsRead` the controller calls `markRoomAsRead` once the first page
* `jumpToMessage(id)` loads older pages until the message is found (at most `maxPages`, default 20) and scrolls to it.
* `SupabaseChatCore.instance.searchMessages(roomId, query)` returns the text messages containing `query` (case-insensitive), newest first.

### Forwarding

`SupabaseChatCore.instance.forwardMessages(from, messages, toRoomId)` sends `messages` of room `from` to room `toRoomId` as new messages of the logged-in user, who must be a member of both rooms:

```dart
await SupabaseChatCore.instance.forwardMessages(room, selectedMessages, targetRoomId);
```

* The copies are sent oldest first, a millisecond apart, so they keep their order.
* Each copy keeps the content and the metadata, without the reply, the reactions, the pin and the edited mark.
* Its metadata stores a `ChatForwardOrigin` under `forwardedFrom`: the original author's id and name and the original date. Read it with `ChatForwardOrigin.of(message)`, which returns `null` for messages that weren't forwarded, e.g. to show a "Forwarded from ..." header. Forwarding a forwarded message keeps the first origin.
* The author's name is a snapshot taken when forwarding: the recipients may not share a room with the original author, so they can't read their profile.
* Attachments stored in the chat assets bucket are copied into `toRoomId`'s folder, since the Storage policies only let members of a room read its files; other URLs are kept as they are.
* `TextStreamMessage`s and `UnsupportedMessage`s can't be stored and are skipped.

### Typing indicator

Call `onTyping()` on every keystroke of the composer (throttling is internal) and listen to `typingUsers`, a stream of the other members currently typing (`currentTypingUsers` for the current value):
Expand Down
2 changes: 1 addition & 1 deletion doc/docs/guides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: testing
title: Testing and development
---

The repository has three test suites and a few scripts to run them against a local Supabase stack, started with the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) (`supabase/config.toml` exposes the `chats` schema). The scripts are Bash and need `psql`, `curl` and Docker.
The repository has three test suites and a few scripts to run them against a local Supabase stack, started with the [Supabase CLI](https://supabase.com/docs/guides/local-development/cli/getting-started) (`supabase/config.toml` exposes the `chats` schema). The scripts are Bash and need `psql`, `curl` and Docker.

## Unit tests

Expand Down
2 changes: 1 addition & 1 deletion doc/docs/introduction/supabase-package-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Requirements: `Dart >=3.8.0` and `Flutter >=3.32.0`.

```yaml
dependencies:
flutter_supabase_chat_core: ^2.0.0-rc.1
flutter_supabase_chat_core: ^2.0.0-rc.2
supabase_flutter: ^2.17.2
uuid: ^4.5.1

Expand Down
9 changes: 2 additions & 7 deletions doc/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const config: Config = {
docs: {
sidebarPath: './sidebars.ts',
editUrl:
'https://github.com/insideapp-srl/flutter_supabase_chat_core',
'https://github.com/insideapp-srl/flutter_supabase_chat_core/tree/main/doc/',
routeBasePath: '/',
},

Expand Down Expand Up @@ -80,12 +80,7 @@ const config: Config = {
position: 'left',
label: 'Tutorial',
},
{to: 'https://docs.flyer.chat/flutter/chat-ui/', label: 'Chat UI', position: 'left'},
{
to: 'https://docs.flyer.chat/flutter/firebase/firebase-overview',
label: 'Firebase Core',
position: 'left'
},
{to: 'https://flyer.chat/docs/flutter/introduction/', label: 'Chat UI', position: 'left'},
{
href: 'https://github.com/insideapp-srl/flutter_supabase_chat_core',
label: 'GitHub',
Expand Down
Loading
Loading