Skip to content

Add a Share button and dialog for sharing a collection (BL-16673) - #8394

Draft
JohnThomson wants to merge 23 commits into
masterfrom
BL-16673-share-dialog
Draft

JohnThomson wants to merge 23 commits into
masterfrom
BL-16673-share-dialog

Conversation

@JohnThomson

@JohnThomson JohnThomson commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem. Bloom already lets a team work on a collection together through Team Collections, but only by having an administrator share a folder with a file-sync service such as Dropbox. Bloom has no control over that sharing: who can reach the folder, and what they may do there, is up to the sync service and the administrator's skill with it. Cloud Team Collections (BL-16531) keep the same way of working, but put the shared copy on a backend Bloom controls (Supabase), which we hope will also be more reliable. That control is what makes it possible for Bloom to have its own UI for sharing: administrators invite people from inside Bloom and decide what each may do. This PR adds that UI, which will eventually replace the Team Collection settings panel. The backend ships separately, from the bloom-core-supabase project, and isn't deployed yet.

What the PR does.

  • Share button: a new button in the Collection tab's top bar opens a Share dialog, built from the mockups on the card.
  • Admins: can invite people by email as Admin or Editor, change other members' roles from a menu that describes each role, and remove people. Nobody can change their own role or remove themselves, so an admin steps down only once someone else has taken over, and a collection always keeps an admin.
  • Learn about sharing: a link in the dialog opens the help page in the browser.
  • Editors: see the member list read-only, with "Only admins can invite people or change roles."
  • Identity: people are identified by their BloomLibrary.org sign-in, which the cloud backend will authenticate. A signed-out user is asked to sign in first.
  • Starting to share. Nothing is saved until the admin presses Start sharing. Before that, the dialog explains what sharing does and lists who will have access: the admin, and for a folder Team Collection everyone its history shows, as Admin if the Team Collection lists them as an administrator and otherwise Editor. For a Team Collection it states the consequences (the old collection is frozen for everyone, and people need Bloom 6.6 to continue) and asks for confirmation. After the start, invites, role changes and removals take effect immediately, and each member shows when they were last seen, or else when they were invited. Freezing the old collection, requiring Bloom 6.6, creating the cloud collection and uploading the books are separate steps in CollectionSharingStarter, stubbed until the backend and the freeze (BL-16928 Honor no-shared-folder-changes flag #8414) land.
  • Stand-in backend: SharingApi talks to an ICollectionSharingService. Its only implementation keeps the record in sharing.local.json in the collection folder (a folder TC doesn't sync it) and enforces the rules the backend's RPCs will, with invitations all-or-nothing and safe writes. Invitations aren't sent anywhere yet; migrating TC members and uploading books wait for the backend.
  • Button icons: TopBarButton and BloomButton can take an icon element (the MUI Share icon) instead of an image file.
  • Design overview: Design/CloudTeamCollections.md is a new, current overview of Cloud Team Collections as a whole: architecture, checkout and check-in, the database, the server API, how a collection starts being a cloud collection (initial upload and migrating a folder Team Collection), and what is still open. It says where each part is being built (the server, the Cloud Team Collections (S3 + Supabase) #8052 client, this dialog, the BL-16928 Honor no-shared-folder-changes flag #8414 freeze).

Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16673

🤖 Generated with Claude Code


Devin review


This change is Reviewable

JohnThomson and others added 2 commits September 23, 2026 11:18
The Collection tab gets a Share button that opens a dialog where a
collection's admins invite people by email as Admins or Editors, change
their roles, and remove them; editors see the list read-only. People are
identified by their BloomLibrary.org sign-in, so the dialog asks a
signed-out user to sign in first. Inviting the first person is what
shares a collection, and in a Team Collection only its administrators
can do that. For a Team Collection, the people its history shows have
worked in it are offered as suggestions to invite.

The cloud (Supabase) backend is not deployed yet, so SharingApi talks to
an ICollectionSharingService whose one implementation keeps the record in
sharing.local.json in the collection folder (a folder TC does not sync
it) and enforces the rules the backend will: only admins change
anything, the last admin can be neither demoted nor removed, and nobody
is invited twice.

TopBarButton and BloomButton can now take an icon element (here the MUI
Share icon) instead of an image file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…16673)

Fixes from Devin's review of the Share dialog:

- Inviting several people (the "Invite Selected" suggestions) is now one
  operation: the service checks every invitation against the record
  before adding any, so one that is already a member no longer leaves
  the earlier ones saved and the open dialogs unrefreshed.
- A failed invitation no longer dismisses the suggestions the admin left
  unchecked. postJson reports a failure and then resolves anyway, so
  invite() now resolves to whether it worked.
- "Last seen" is recorded when the collection opens, not only when
  someone opens the Share dialog.
- sharing/state runs on the UI thread, like teamCollection/getHistory,
  because its suggestions read the collection's book list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/BloomBrowserUI/sharing/sharingApi.ts Outdated
Comment thread src/BloomExe/web/controllers/SharingApi.cs Outdated
Comment thread src/BloomExe/web/controllers/SharingApi.cs
Comment thread src/BloomExe/web/controllers/SharingApi.cs
Comment thread DistFiles/localization/en/BloomMediumPriority.xlf
Comment thread src/BloomBrowserUI/sharing/relativeTime.ts
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-23 11:30 up to commit fef96dd. It raised 3 bugs (all fixed in 9d38ddf) and 3 points to check (answered, no change needed); each has its own resolved thread above. The fix commit gets its own Devin run.

Comment thread src/BloomExe/web/controllers/SharingApi.cs Outdated
Comment thread src/BloomExe/web/controllers/SharingApi.cs
Comment thread src/BloomBrowserUI/sharing/ShareDialog.test.tsx
Devin's re-review found that the first invitation, which is what shares
a collection, saved the new sharing record and the invitees separately;
if adding the invitees failed, the collection was left shared with only
the admin. StartSharing now takes the first invitations and writes the
record once, after checking every invitation, so a bad one leaves the
collection unshared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-23 11:47 up to commit 9d38ddf. It confirmed the three earlier bugs fixed and found one new one (fixed in dd4f70e); it also asked about the target branch and the test runs (answered). Each has its own resolved thread.

From Devin's third review:

- The sharing file is now written to a temporary file that replaces the
  real one only when complete, so Bloom stopping part way through a save
  can't leave a truncated file that breaks the Share dialog.
- Both ways of inviting (the email box and Invite Selected) are disabled
  while an invitation is on its way, so a double click can't send the
  same people twice, and the email box keeps what was typed if the
  invitation fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/BloomExe/Sharing/LocalFileCollectionSharingService.cs
Comment thread src/BloomBrowserUI/sharing/ShareDialog.tsx Outdated
Comment thread src/BloomExe/Sharing/LocalFileCollectionSharingService.cs
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-23 11:56 up to commit dd4f70e. It confirmed the earlier fixes and raised two new bugs (fixed in ea412e3) and one point about collection archives (answered). Each has its own resolved thread.

A successful invitation clears the email box, so a next address typed
while it was on its way was wiped out (found by Devin). The box is now
disabled until the answer comes back, like the Invite button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/BloomBrowserUI/sharing/ShareDialog.tsx
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-23 12:05 up to commit eb0801d. Re-review clean: all 7 bugs it raised across the rounds are marked fixed, and its remaining points repeat ones already answered in their threads.

Decisions from the preflight report:

- An admin can no longer change their own role or remove themselves;
  another admin has to. So an admin can only step down once someone else
  has actually taken over, and a shared collection always keeps an admin,
  which makes the separate last-admin checks unnecessary. In the dialog,
  your own role shows as plain text with a tooltip explaining why.
- The dialog's title bar now has the "Learn about sharing" link from the
  mockups. For now it opens the Team Collections introduction on
  docs.bloomlibrary.org, in the web browser.

The Sharing.ShareDialog.LastAdmin string is no longer used and is
removed. It was added on this branch, was always translate="no", has
never been on master, and appears in no translated file, so removing it
loses no translations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-23 15:13 up to commit c65cc3f (John's decisions: no changing your own role; the Learn about sharing link). Re-review clean: no new bugs, all 7 earlier ones marked fixed; its other points repeat ones already answered.

@JohnThomson JohnThomson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JohnThomson reviewed 15 files and all commit messages, made 6 comments, and resolved 2 discussions.
Reviewable status: 0 of 18 files reviewed, 17 unresolved discussions.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 45 at r3 (raw file):

const kSecondaryTextColor = "#757575";
const kDangerColor = "#d32f2f";

If these colors are close to colors Bloom already uses, they are probably meant to be the same as those colors, and should use existing constants.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 290 at r3 (raw file):

    );
    const notSharedNote = useL10n(
        "This collection is not shared yet. Only an administrator of this Team Collection can share it.",

We may not continue to use the term "Team Collection", thinking of this more as a shared collection. I think in this context we can just say, "...of this Collection..." There may be other new strings where we can avoid using the old term.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 358 at r3 (raw file):

                setEmail("");
                setTriedInvalid(false);
            }

I think the intention is that Invite, besides doing something in the cloud to record that this person is allowed to use the collection, would add this entry to the list. Should we do that here, or do we get an updated list of members as a side effect of calling props.onInvite?


src/BloomBrowserUI/sharing/ShareDialog.tsx line 431 at r3 (raw file):

        "These people have worked on this Team Collection. Do you want to invite them?",
        "Sharing.ShareDialog.SuggestionsHeading",
    );

I don't think we need this heading. The people we discover in history don't need to be suggestions; they simply are the people who have permission to use this cloud collection, unless the admin removes them. (Of course the admin can also add more.)
(This might change if the UI designer decides that Invite actually sends some kind of invitation. I think the intent is that it just puts someone in the list of those permitted access to the collection in whatever way the admin specified. The invitation is 'accepted' only in the sense that at some point the invitee opens the collection from the cloud. We might need a backend change to know that has happened.)


src/BloomBrowserUI/sharing/ShareDialog.tsx line 594 at r3 (raw file):

        props.now,
        props.uiLanguage,
    );

If my interpretation is right, we don't need member.invited; anyone in the list of MemberRows is invited.
If someone is known from the database to have accessed the collection, lastSeen would be the time they last did so (that is, the last time this user opened the collection somewhere or made a change to it). If they have never done so but are known from history to have checked a book in, lastSeen would be the last time they did that. If they have not done either, then they are considered to have been invited at the time the admin gave them permission to access the collection.


src/BloomBrowserUI/sharing/sharingApi.ts line 25 at r3 (raw file):

// Someone the old Team Collection's history shows has worked in this collection, whom an
// admin may want to invite.
export interface ISharingSuggestion {

We might not need this if historic users are automatically made sharing members

…nd member status (BL-16673)

Reworks the Share dialog per John's review of PR #8394. The current goal is sharing a
collection that is not already a Team Collection; migration comes later.

Team Collection history seeds the members instead of being offered as suggestions:
- The history scan (SharingSuggestions.Find, renamed TeamCollectionHistoryMembers.Find)
  now gives the people who should simply have access. When a signed-in person who may manage
  sharing (the existing CanManage rule) first asks for sharing/state on a (folder) Team
  Collection with no sharing record, the server shares it then
  (TeamCollectionHistoryMembers.StartSharingIfTeamCollection): the admin plus everyone the
  history shows, as Admin if the TC's Administrators list has them, otherwise Editor, all in
  one write. StartSharing takes these history members (name, role, last activity) alongside
  the invitations; the signed-in admin is never added twice. An ordinary collection is
  unchanged: it starts being shared with the first invitation. The admin removes anyone who
  should not be there.
- Removed everything that existed only for the suggestions panel: the SuggestionsPanel UI,
  `suggestions` in the state, ISharingSuggestion / SharingSuggestion, dismissSuggestions (TS
  API, the sharing/dismissSuggestions endpoint, the service's DismissSuggestions, and the
  record's DismissedSuggestions), and their tests.

No member status: everyone in the list is invited.
- Removed SharingMemberStatus / status / data-status. Each member has invitedAt and a nullable
  lastSeen: RecordVisit sets it, and history-seeded members start with their last history
  activity. A row shows "Last seen <when>" if lastSeen is set, otherwise "Invited <when>";
  the faded avatar/name for someone never seen now follows from lastSeen being missing.

Other review points:
- Colors: the dialog's own #757575 and #d32f2f are replaced by Bloom's kMutedTextGray and
  kBloomRed; while there, the #f1f3f4 note background and #e0e0e0 dividers use
  kFormBackground and kBannerGray.
- "Team Collection" is gone from the Sharing strings: OnlyAdminsCanShare now reads "This
  collection is not shared yet. Only an administrator of this collection can share it.", and
  the matching C# exception message says "collection" too.
- A new member row comes only from the server: a successful invite makes SharingApi send the
  sharing/stateChanged websocket event, and useSharingState (useWatchApiData) then fetches
  sharing/state again. Noted in a comment at the invite; no optimistic insert.

XLF (DistFiles/localization/en/BloomMediumPriority.xlf, all translate="no"):
- Deleted Sharing.ShareDialog.SuggestionsHeading, NotNow and InviteSelected, which only the
  removed panel used. They were added on this branch (fef96dd) and never shipped:
  `git log -G '<id>' origin/master -- DistFiles/localization/en/BloomMediumPriority.xlf` and
  the same over origin/Version6.5 find no commits for any of the three, and no translated
  folder under DistFiles/localization has them, so deleting loses nothing.
- Changed the OnlyAdminsCanShare text and note, and updated the LastSeen and InvitedWhen
  notes to say what each time means.

Tests: C# FullyQualifiedName~BloomTests.Sharing (31 pass) now covers seeding (a TC with
history is shared at the first admin look with the right roles and lastSeen; an ordinary
collection stays unshared and reads no history; an already-shared TC is left alone so removed
people do not come back; the admin is not duplicated) and lastSeen after a visit. The vitest
dialog tests check the "Last seen"/"Invited" text, using a useL10n mock that returns the
English with its parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@JohnThomson JohnThomson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JohnThomson+AGNT made 6 comments and resolved 6 discussions.
Reviewable status: 0 of 20 files reviewed, 11 unresolved discussions.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 45 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

If these colors are close to colors Bloom already uses, they are probably meant to be the same as those colors, and should use existing constants.

[Claude Opus 5.5] Done in 122b0cd. No local colors are left. #757575 is now kMutedTextGray and the danger red is now kBloomRed (#d65649, the same color kWarningColor uses). I also switched the note background to kFormBackground and the dividers to kBannerGray.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 290 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

We may not continue to use the term "Team Collection", thinking of this more as a shared collection. I think in this context we can just say, "...of this Collection..." There may be other new strings where we can avoid using the old term.

[Claude Opus 5.5] Done in 122b0cd. The note now reads "This collection is not shared yet. Only an administrator of this collection can share it." The matching C# error message changed too. None of the other new sharing strings say "Team Collection".


src/BloomBrowserUI/sharing/ShareDialog.tsx line 358 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

I think the intention is that Invite, besides doing something in the cloud to record that this person is allowed to use the collection, would add this entry to the list. Should we do that here, or do we get an updated list of members as a side effect of calling props.onInvite?

[Claude Opus 5.5] We get the updated list as a side effect. A successful invite on the C# side writes the record and then sends the sharing/stateChanged websocket event. The dialog gets its data from useWatchApiData, which listens for that event and fetches sharing/state again, so the new row comes from the server. There is no optimistic insert here. I added a comment at tryInvite saying so. When the real backend arrives, the same event (or its realtime equivalent) will do this job.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 431 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

I don't think we need this heading. The people we discover in history don't need to be suggestions; they simply are the people who have permission to use this cloud collection, unless the admin removes them. (Of course the admin can also add more.)
(This might change if the UI designer decides that Invite actually sends some kind of invitation. I think the intent is that it just puts someone in the list of those permitted access to the collection in whatever way the admin specified. The invitation is 'accepted' only in the sense that at some point the invitee opens the collection from the cloud. We might need a backend change to know that has happened.)

[Claude Opus 5.5] Agreed, and done in 122b0cd. Suggestions are gone completely: the panel, the heading, dismissing, and their strings. The first time an administrator opens Share on a Team Collection that is not shared yet, it is shared right away. The members are that admin plus everyone the history shows. They are Admin if the TC lists them as an administrator, otherwise Editor, and the admin can remove anyone. An ordinary collection still starts being shared with the first invite. Invite only adds someone to the list of people allowed in, as you describe. A backend change to notice when an invitee first opens the collection can come later.


src/BloomBrowserUI/sharing/ShareDialog.tsx line 594 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

If my interpretation is right, we don't need member.invited; anyone in the list of MemberRows is invited.
If someone is known from the database to have accessed the collection, lastSeen would be the time they last did so (that is, the last time this user opened the collection somewhere or made a change to it). If they have never done so but are known from history to have checked a book in, lastSeen would be the last time they did that. If they have not done either, then they are considered to have been invited at the time the admin gave them permission to access the collection.

[Claude Opus 5.5] Done in 122b0cd. The invited/active status is gone. Each member has invitedAt and an optional lastSeen. lastSeen is set when they open the collection, which for now means this Bloom recording a visit. For people brought in from history, it starts as their last history event. A row shows "Last seen ..." when lastSeen is known, otherwise "Invited ...". One small difference from what you wrote: the history time is their last recorded event of any kind, not only a check-in. The faded look for someone never seen now follows from lastSeen being empty.


src/BloomBrowserUI/sharing/sharingApi.ts line 25 at r3 (raw file):

Previously, JohnThomson (John Thomson) wrote…

We might not need this if historic users are automatically made sharing members

[Claude Opus 5.5] Right. ISharingSuggestion and the rest of the suggestions API were removed in 122b0cd.

Comment thread src/BloomExe/web/controllers/SharingApi.cs Outdated
Comment thread src/BloomExe/Sharing/TeamCollectionHistoryMembers.cs Outdated
Comment thread src/BloomBrowserUI/sharing/relativeTime.ts
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5] Consulted Devin on 2026-09-25 up to commit 122b0cd. It raised one new bug (a first invite arriving before the dialog loads, which the dialog can't do) and two points to check (history members added without asking, which is intended; pseudo-localized dates, which work). All three are answered in their own resolved threads with no code change. Everything it raised earlier is fixed.

JohnThomson and others added 8 commits September 25, 2026 11:31
Design/CloudTeamCollections.md describes the design as it now stands, for a
developer new to cloud Team Collections, and says where the detailed docs live
(bloom-core-supabase PR #13's team-collections/docs, and the Design folder on
the #8052 branch), since this file reaches master first.

It covers the goals and architecture; identity, membership and roles, with
the Share dialog as built on this branch and the sharing UI planned on the
Sharing cards (BL-16672/4/5/6, BL-16527), each labelled as such; the book
lifecycle with the client-made checkout GUID replacing the seat (write-ahead
.checkout, hash on the server, obsolete-record cancellation), two-phase
check-in transactions, send-only locks, unconfirmed check-ins, receive,
polling, Lost and Found, force unlock; transferring a local checkout to a new
login through checkout_book_takeover, with what is decided and what is open;
the tc schema as a Mermaid ER diagram checked against 03_tables.sql; the RPC
and edge-function surface in summary; open questions (including where the
server does not yet match the Share dialog's rules); and a short section on
what changed since the July version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A semicolon ends a statement in a Mermaid sequence diagram, so the message
"keep the file; next poll/open decides" split in two and the diagram failed to
parse. All four diagrams in the doc now render with mermaid-cli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inviting only adds an address to the collection's allowed list; no email is sent.
  A signed-in person allowed to use a cloud collection they haven't joined sees an
  invitation card in Open/Create Collections. Dropped 'should invitations be emailed'
  from the open questions.
- Cloud Team Collections aren't in 6.5 and are to be fully live (perhaps still
  experimental) in 6.6; nothing is gated by the cloudCollections environment variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend (bloom-core-supabase #13, 4d1eb61) now records when each member last had a
collection open, set by get_collection_state and get_changes at most every 10 minutes.
Add it to the schema diagram and the members notes, and replace the open question about
last seen with the two follow-ups that remain (history times on migration; a touch point
if realtime replaces polling).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6676)

Add section 5, "Starting a cloud collection: initial upload and migration",
marked designed but not built. It covers sharing an ordinary collection and
moving a folder Team Collection to the cloud, done by the admin's 6.6 Bloom:

- freeze the old shared folder with AllowSharedFolderChanges=False (the
  BL-16928 6.5 patch) plus AllowCheckouts=False for older Blooms;
- create the cloud collection with an initial-upload flag and send books in
  the background as ordinary first check-ins, so the admin is never frozen;
- for books others hold in the old system, write a Migration Keys/<instanceId>.json
  key (checkout GUID, old email, old machine) and lock the book to a
  legacy:<old email> placeholder with the GUID's hash;
- clear the flag and write the cloud id into the old shared settings, after
  which each member's Bloom switches over and moves its checkouts with
  checkout_book_takeover;
- recovery by a support script if an upload is abandoned.

Renumber the later sections, point the BL-16676 planned-UI bullet and the
open-questions list at the new section (which replaces the mockup's
wait-for-everyone-to-check-in phase), list the server and client work it
needs, and drop a stale note saying tc.members has no last-seen column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…migration

In section 5, step 1, right after AllowSharedFolderChanges and AllowCheckouts
are set, the old shared folder is made read-only for everyone except the
migrating admin (Dropbox "Can view" set by the folder's owner, or LAN
file-system permissions), as a safeguard against Blooms too old to honor those
settings or MinimumBloomVersion. It is a manual checklist step Bloom cannot do
or verify; later writes to the old folder are all the admin's, and switched
6.6 members only read it. The recovery step restores write access too.

Section 9 gets the open question of whether this makes an old Bloom fail fast:
a LAN share refuses the write, but a Dropbox "Can view" folder may accept the
local write and never upload it, which needs testing with two Dropbox accounts
(and OneDrive/Google Drive if supported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-only test

Folder Team Collections don't work over OneDrive or Google Drive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JohnThomson added a commit that referenced this pull request Sep 28, 2026
In Bloom 6.6 an administrator will be able to move a folder Team Collection to
Bloom's cloud collections (BL-16531, BL-16676), and while that happens the old
shared folder must stop changing. AllowCheckouts (BL-16691) only stops new
checkouts, and MinimumBloomVersion (BL-16690) would lock people out of books
they have checked out. This 6.5 patch adds an AllowSharedFolderChanges
collection setting, on the same pattern as AllowCheckouts:

- It defaults to true, is written to the .bloomCollection only while false,
  and is picked up mid-session. The shared folder's copy of the settings
  decides; the local copy is used only when the shared one can't be read, and
  a shared folder with no settings yet isn't paused. So an administrator's
  hand edit is pushed up as usual and then pauses everyone, the administrator
  included.
- While it is false, nothing writes to the shared folder: check in (including
  first check-ins), check out, rename, delete, Forget Changes, Force Unlock,
  collection files and color palettes. TeamCollection's public write methods
  refuse (SharedFolderChangesPausedException) before doing anything, the
  low-level FolderTeamCollection writes check again, the API layer refuses and
  asks the status panel to redraw, and SyncAtStartup reports a refused per-book
  fix-up as a warning rather than an error.
- Editing books already checked out keeps working, as does deleting a book
  that was never checked in. Collection Settings and the reader-tool settings
  refuse to open, since their changes couldn't be shared and would be replaced
  by the shared copy at the next start.
- The book status panel disables the affected buttons and explains why. If the
  shared settings carry a CloudCollectionId (which the 6.6 migration will
  write), the message says the collection has moved to Bloom's cloud sharing
  and Bloom 6.6 is needed; otherwise that the administrator has paused changes.
  The two new strings (TeamCollection.SharedFolderChangesPaused and
  TeamCollection.MovedToCloud) and BL-16691's paused-checkouts note
  (TeamCollection.CheckoutsPaused, previously English-only) are live,
  translatable entries in Bloom.xlf.
- A new Team Collection made from a copy of a paused collection drops the
  pause and cloud id before its settings go up, so it starts out unpaused
  with its books.

There is no UI for setting it; the 6.6 migration will set it (see
Design/CloudTeamCollections.md, PR #8394). 38 new C# tests cover the settings
round trip, each write path being refused when paused (with the shared folder
unchanged) and working when allowed, the shared folder's authority, and the
mid-session pickup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JohnThomson and others added 2 commits September 28, 2026 07:24
…eeze PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…NTRACTS v1.12)

Name the RPCs as built in bloom-core-supabase #13, say the placeholder is guarded by a
CHECK on members.user_id rather than by account ids never containing ':', record that the
placeholder CheckOut event names the admin with the placeholder in lock_info, and note that
my_collections also hides a collection from the uploading admin's other computers and that the
support delete should wait until the admin's Bloom has stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JohnThomson added a commit that referenced this pull request Sep 28, 2026
When an administrator moves a folder Team Collection to Bloom's cloud
collections (BL-16531, BL-16676), the old shared folder must stop changing
while the books are uploaded. Blooms older than 6.6 are shut out of the
collection by the existing MinimumBloomVersion gate (BL-16690), which the
migration sets to 6.6; this setting is for 6.6 Blooms that are still on the
old system until the upload finishes. It adds an AllowSharedFolderChanges
collection setting, on the same pattern as AllowCheckouts (BL-16691):

- It defaults to true, is written to the .bloomCollection only while false,
  and is picked up mid-session. The shared folder's copy of the settings
  decides; the local copy is used only when the shared one can't be read, and
  a shared folder with no settings yet isn't paused. So an administrator's
  hand edit is pushed up as usual and then pauses everyone, the administrator
  included.
- While it is false, nothing writes to the shared folder: check in (including
  first check-ins), check out, rename, delete, Forget Changes, Force Unlock,
  collection files and color palettes. TeamCollection's public write methods
  refuse (SharedFolderChangesPausedException) before doing anything, the
  low-level FolderTeamCollection writes check again, the API layer refuses and
  asks the status panel to redraw, and SyncAtStartup reports a refused per-book
  fix-up as a warning rather than an error.
- Editing books already checked out keeps working, as does deleting a book
  that was never checked in. Collection Settings and the reader-tool settings
  refuse to open, since their changes couldn't be shared and would be replaced
  by the shared copy at the next start.
- The book status panel disables the affected buttons and explains why. If the
  shared settings carry a CloudCollectionId (which the migration writes when
  the upload is done), the message says the collection has moved to Bloom's
  cloud sharing; otherwise that the administrator has paused changes. The two
  new strings (TeamCollection.SharedFolderChangesPaused and
  TeamCollection.MovedToCloud) and BL-16691's paused-checkouts note
  (TeamCollection.CheckoutsPaused, previously English-only) are in Bloom.xlf,
  translate="no" while the feature is in development on master.
- A new Team Collection made from a copy of a paused collection drops the
  pause and cloud id before its settings go up, so it starts out unpaused
  with its books.

There is no UI for setting it; the migration will set it (see
Design/CloudTeamCollections.md, PR #8394). 38 new C# tests cover the settings
round trip, each write path being refused when paused (with the shared folder
unchanged) and working when allowed, the shared folder's authority, and the
mid-session pickup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JohnThomson and others added 5 commits September 28, 2026 12:39
…e is master-only

The AllowSharedFolderChanges freeze (BL-16928, #8414) no longer ships as a 6.5 patch. The
migration now sets MinimumBloomVersion=6.6 in the old shared settings, which 6.4 and 6.5
already honor by shutting the collection, so their users upgrade before continuing, checked-out
books included; AllowSharedFolderChanges=False is for 6.6 Blooms still on the old system during
the upload, and AllowCheckouts is no longer needed. Every collection that becomes a cloud
collection, Team Collection or not, also gets MinimumBloomVersion=6.6 in its own settings, so
every member's copy shuts out older Blooms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dialog has no Cancel, so opening it must not start sharing, and a
collection should not become shared as a side effect of its first
invitation. Now, before a collection is shared, the dialog is a preview
and saves nothing:

- It explains what sharing does, lists read-only who will have access
  (you as Admin, plus, for a folder Team Collection, everyone its history
  shows, Admin if in its administrators list, otherwise Editor), and for a
  Team Collection states the consequences: frozen for everyone, people
  keep their checkouts but need Bloom 6.6, older Blooms will not open it,
  and it can't be undone from Bloom.
- Start sharing is enabled only for someone signed in who may manage the
  collection (existing CanManage rule); otherwise the sign-in prompt or
  the "only an administrator can share it" note says why.
  SharingApi.CanStart is the seam for later conditions (subscription,
  BL-16672; being online).
- For a Team Collection, Start asks for confirmation in a small
  BloomDialog (Start sharing / Cancel); an ordinary collection starts at
  once.
- The invite box appears only after sharing has started.

API: sharing/state no longer starts sharing a Team Collection; when
unshared it writes nothing and returns isTeamCollection, canStart and
previewMembers. New POST sharing/start (UI thread, as it reads history)
requires CanManage and is refused if already shared. sharing/invite on an
unshared collection is now an error (the service already throws).
ICollectionSharingService.StartSharing drops its invitations parameter;
CollectionSharingRecord.StartingMembers builds the members for both the
preview and the saved record. TeamCollectionHistoryMembers
.StartSharingIfTeamCollection is gone.

The new CollectionSharingStarter runs the steps of starting in the order
of Design/CloudTeamCollections.md section 5, with empty, documented
methods for the parts not built yet: FreezeOldTeamCollection (needs
AllowSharedFolderChanges from #8414), RequireNewerBloomForCloudCollection
(MinimumBloomVersion=6.6 in the collection's own settings; deliberately
not done by the stand-in, since without a real cloud collection it would
only lock older Blooms out of this collection here and, for a Team
Collection whose settings get pushed to the shared folder, lock every
6.5 teammate out), and StartInitialUpload. No upload progress is shown.

Strings: nine new Sharing.ShareDialog.* entries in
BloomMediumPriority.xlf, all translate="no" (branch-only, master target),
with notes. No existing string became unused, so none are removed or
marked obsolete.

Tests: new CollectionSharingStarterTests (preview writes nothing and
lists admin + history people / just the admin; start saves what was
previewed; start requires CanManage; starting twice is refused and
re-adds nobody); service tests updated for the new StartSharing
signature. ShareDialog.test.tsx covers the preview, the Team Collection
consequences and confirmation (confirm and cancel), disabled Start with
its reason, and the invite box only once shared.

The design doc's Sharing UI section describes the Start sharing flow and
the seams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Disconnected mode

Matches #8414 as rewritten (dd15d97).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmary

The summary table left out finish_initial_upload, lock_book_for_legacy_checkout
and support_delete_collection, and create_collection's initial-upload flag, all
described in section 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/BloomExe/Sharing/CollectionSharingStarter.cs
Comment thread src/BloomExe/web/controllers/SharingApi.cs
/>
</div>
)}
{props.state.isTeamCollection && <TeamCollectionConsequences />}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Devin] Investigate: Sharing dialog promises unavailable migration

The dialog promises cloud access and a frozen Team Collection, but Start currently writes only a local membership file. Review the user-facing wording or availability before release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Opus 5.5 during preflight] Fair point, left open for John to decide. The wording describes what Start will do once the backend and the freeze (#8414) land; the stand-in behind it is known. What's worth a decision is that the Share button isn't behind any flag, so anyone running a master build can press Start and be told their Team Collection is frozen when nothing has happened to it.

Comment thread src/BloomBrowserUI/sharing/relativeTime.ts
Comment thread Design/CloudTeamCollections.md
Comment thread Design/CloudTeamCollections.md
@JohnThomson

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5.5 during preflight] Consulted Devin on 2026-09-29 up to commit ee7a170 (the Start sharing change, a merge of master, and a design-doc fix). It re-reviewed the whole PR and raised two new bugs and four new points to check, each in its own thread:

Everything else it raised repeats points already answered, or bugs it now considers fixed.

Adds three "Proposed" subsections to section 9, plus a diagram of the
schema with all of them applied:

- Users with their own identity: a core.users table (id,
  authentication_id, email, name) that the ten identity columns point at,
  so an email change or a new Firebase account doesn't orphan
  memberships, locks and history. Decided: we need it, and the name lives
  on it, taken from Bloom's Registration dialog at every sign-in, with no
  UI for editing other people's names. Unclaimed user rows replace the
  legacy:<email> lock holders of migrated checkouts.
- Book names that needn't be unique: each copy picks its own folder name,
  adding a suffix when needed, and the main .htm is stored as index.htm so
  folder names never show up in the manifest.
- Schema simplifications: drop tc.versions (the CheckIn event already
  records it) and number versions per book; drop redundant ids and
  indexes; resume a check-in attempt only if identical, else abort it, and
  drop revision; name books in the API by instance id; rename
  checkin_transactions, events and version_files to checkin_attempts,
  history_events and book_files, and delete attempt rows once nothing
  needs them; one set of collection files instead of three groups, sent
  only by an admin.

Also fixes section 6 and 4, which said check-in transaction rows are
reaped; they are only marked expired and never deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JohnThomson added a commit that referenced this pull request Oct 9, 2026
When an administrator moves a folder Team Collection to Bloom's cloud
collections (BL-16531, BL-16676), the old shared folder must stop changing
while the books are uploaded. Blooms older than 6.6 are shut out by the
existing MinimumBloomVersion gate (BL-16690), which the migration sets to 6.6.
This setting is for 6.6 Blooms still on the old folder Team Collection until
the upload finishes: they must not write anything to the shared folder, but
their users keep editing the books already checked out to them and any new
local books.

That is exactly what Disconnected mode already provides, so this reuses it:

- AllowSharedFolderChanges (default true, written to the .bloomCollection only
  while false) and CloudCollectionId (written only when present) are new
  collection settings, read and written like AllowCheckouts, so Save() never
  drops them and an administrator's hand edit is pushed up as usual.
- FolderTeamCollection.CheckConnection(), which runs at startup and before
  every check-out, check-in, Forget Changes and Force Unlock, reads them from
  the shared folder's copy of the settings after its existing folder, network
  and Dropbox checks. A pause is reported as the connection problem:
  TeamCollection.SharedFolderChangesPaused, or TeamCollection.MovedToCloud once
  a cloud id is present (close and reopen the collection to switch over).
  Missing or unreadable settings never cause a disconnect.
- TeamCollectionManager.MakeDisconnected recognizes those two messages, records
  the reason on the DisconnectedTeamCollection (as it does for a lapsed
  subscription) and leaves out the usual "resolve this problem and click
  Reload" line.
- A running Bloom picks the pause up when a teammate's settings change arrives
  (HandleCollectionSettingsChange, on the UI thread in the Idle handler, where
  a newly required minimum version is already handled). That path checks only
  the pause, not the whole connection, so a momentary network or Dropbox blip
  can't leave an ordinary collection disconnected.
- The book status panel shows the pause or moved message in place of the
  generic disconnected wording. BL-16691's paused-checkouts note is now
  localized too (TeamCollection.CheckoutsPaused).
- The three strings are translate="no" in Bloom.xlf while the feature is in
  development on master.

There is no UI for setting the flag; the migration (the Start sharing work on
#8394) will set it. Tests cover the settings round trip, CheckConnection's
answers for unpaused, paused, moved and unreadable settings, MakeDisconnected
for each reason, and the mid-session pickup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant