Conversation
The @mention picker needs to find a group's members by any word of their name or their handle, locally. Adds chat_member_search_tokens (chat_id, user_id, token), indexed on (chat_id, token), so a query is an index range scan: token >= q AND token < q || U+10FFFF. The bound is U+10FFFF rather than U+FFFF so a token continuing with an emoji still falls inside it. Tokens are the display name split on whitespace plus the handle without its @, folded by NFKD, then root-locale lowercase, then dropping Mn marks, so "eri" finds "Érica". Every writer of members or profiles keeps them in step: roster upserts, removals, UserProfileDataSource.store and blocked users. chat_roster_sync records how far each group's roster has been read. It is its own table because chat_metadata is rewritten whole from each feed payload. Both tables arrive in an additive 40 -> 41 auto-migration.
GetRoster returns at most 100 members a page, and until now only the first page was ever read. RosterSync walks the pages to has_more = false, capped at 20 pages, when a group opens and the device holds fewer members than member_count, has never read it, or the stream skipped a roster version. It runs off the screen's path and never on launch. A read drops absent members only when every page reported one roster version and the stream has not moved past it. RosterSearchSource is the seam the picker will call; LocalRosterSearchSource is the only implementation. It excludes the current user and ranks recent speakers among the chat's last 50 held messages first, then an exact handle match, then by folded display name. An empty query returns the recent speakers. refresh() re-reads the first page, since profile edits do not move the roster version.
…rsion Store Member.version on chat_members and merge it greater-wins, so a roster page that trails the stream cannot wind a member back. Replace needs_resync with a watermark, fully_synced and reconcile_pending, and make a full read drop only members who joined at or before the version it read, as the proto's reconcile rule requires. Schema v41 is unreleased, so it changes in place; the 40 to 41 auto-migration still only adds columns and tables.
… WorkManager A missed roster update no longer forces a full reload. On open, or on a version gap in a group already read in full, read GetRoster from the top and stop at the first member at or below the watermark, usually one page. Then compare the held count to member_count: equal moves the watermark to the page's version, more held means someone left, so the chat is marked for reconcile and a full read is queued. The full read runs as unique WorkManager work per chat (KEEP, network required, exponential backoff) through the existing HiltWorkerFactory, and so does the first read of a roster over one page.
…ke iOS A MemberLeft deleted the member row, so a GetRoster page that trailed the stream re-added them at their older join version. model.proto says a trailing page cannot resurrect a member the stream removed. Keep a marker row instead (is_member = 0, version = the leave's roster version, search words dropped). The member upsert leaves a marker in place unless the incoming version is higher, which only a rejoin has. Every read of chat_members skips markers, a feed refresh keeps them, and a complete roster read clears those at or below its version. advancePointer now copies the row rather than rebuilding it, which would have reset the version and re-added the member. Search ranking compares names by code point rather than UTF-16 unit, and breaks the last tie on the lowercase hex user id instead of signed decimal bytes, so both platforms return members in the same order.
MigrationTestHelper on Android reads exported schemas from test assets,
and Room 2.8.5's Android artifact has no schema-directory constructor.
The android { sourceSets } DSL fails with a cast error under AGP 9.4, so
the schemas are added through androidComponents' hostTests variant API.
The test seeds v40 chats, members and messages, migrates with
validateDroppedTables, and checks that row counts hold, existing members
take version=0 and is_member=1, and the new tables and token index exist
and are empty. It also opens the v40 file with the app's own builder:
that builder falls back to destructive migration, so the test asserts
the rows survive rather than only that the open succeeds.
bmc08gt
marked this pull request as draft
October 1, 2026 18:35
Collaborator
Author
|
Parked: |
Collaborator
Author
|
Superseded by #1647, which suggests mentions from the server's GetMentionSuggestions pool instead of a locally synced roster. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The @mention picker needs to search a group's members as someone types, but
chat_membersonly holds the members the app happened to see, andChatController.getRosterreads one page. This PR stores each group's full roster locally with a prefix index and puts search behind an interface. There's no UI yet; the picker is the next PR.No new RPC. It uses the paged
Chat.GetRosterand the versioned MemberJoined/MemberLeft stream updates. iOS implements the same rules in 403e551f, so the two platforms search and rank the same way.Search
chat_member_search_tokens (chat_id_hex, user_id_hex, token), indexed on(chat_id_hex, token). Each word of the display name and the username is a token.token >= q AND token < q + U+10FFFF. A DAO test asserts the query plan uses the index. The upper bound is U+10FFFF rather than U+FFFF so tokens continuing with an emoji still match.@is stripped per query word, so@@eridoes not matcheri.RosterSearchSource.search(chatId, query, limit)is the seam. OnlyLocalRosterSearchSourceships; a server-side search can implement the same interface later.Keeping the roster current
chat_roster_syncstores, per chat, the last roster version fully applied (the watermark), plus whether a full read has completed and whether a reconcile is pending.RosterSyncreads from the top of the roster, which is newest-joined first, and stops at the first member whoseMember.versionis at or below the watermark. That's usually one page. Comparing the stored count withmember_countafter that:roster_summary.version.RosterReconcileWorker, which follows theGiftCardFundingWorkerpattern. It's unique work per chat, needs a network connection, uses exponential backoff, and is capped at 20 pages.is_member = 0, stamped with the leave's version) instead of deleting it. A page that trails the stream can't bring the member back, and a rejoin at a higher version does. Every read ofchat_membersskips markers, and a complete reload clears them.Member.versiononly changes on join. The proto says it will also move on role changes, soRosterSync.catchUphas a REVISIT comment there.Schema
The database goes from version 40 to 41 as an AutoMigration: two new tables, plus
versionandis_membercolumns onchat_memberswith defaults.RosterSearchMigrationTestcovers the migration withMigrationTestHelper. The exported schemas reach it as unit-test assets through theandroidComponentsvariant API, because theandroid { sourceSets }DSL throws a cast error under AGP 9.4. The test also opens a v40 file with the app's own builder and checks that the rows survive, since that builder falls back to destructive migration.