feat(app,learn): invite to a shared studyset by email or link, and join

The apps' half of ADR 0044's step 5 (D37 to D40), on the instance's
half of the commit before.

halkyon_app's SharedSpaceGateway asks for inviting by mail, the
preview of a link, the code proving an address, accepting a mailed
invitation, a space's link (read, switched, joined) and marking a
share seen; SpaceInfo says whether a share is new and who made it,
a member whether they were invited by mail. joinTokenOf reads a link
or its token as pasted. The fake keeps the mails and codes it would
send, for the apps' tests.

In Learn:
- the Owner's Share sheet invites by handle or by email, lists an
  invitation mailed by its role alone, and switches the space's link,
  copying it while on;
- the Library's menu has Join with a link, and a link opened on
  Android (/join/<token> on learn.halkyon.eu, verified through the
  instance's assetlinks) opens the same on a screen of its own: who
  invites and as whom, then Join, or for a mailed invitation the
  address and the code mailed there;
- a studyset someone shared is new until opened: in the Inbox
  ("@alice shared Japanese with you", Open or Leave), on the Profile
  tab's badge and as New on its library card (NewSharesCubit, read
  again after every merge).

Whats-New: Invite people to a shared studyset by email or with a link, join one from the Library's Join with a link, and see in the Inbox what was shared with you.
Whats-New-De: Lade Leute per E-Mail oder Link zu einem geteilten Lernset ein, tritt einem über „Mit einem Link beitreten“ in der Bibliothek bei und sieh im Posteingang, was mit dir geteilt wurde.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-11 19:09:12 +01:00
commit fb6b78fa19
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -2870,5 +2870,81 @@
"studysetViewOnly": "You can view and study this, not change it.",
"@studysetViewOnly": {"description": "Line on a shared studyset where the person is a Viewer"},
"studysetViewOnlyRefused": "You can only view this studyset.",
"@studysetViewOnlyRefused": {"description": "A write in a studyset shared with the person as a Viewer was refused"}
"@studysetViewOnlyRefused": {"description": "A write in a studyset shared with the person as a Viewer was refused"},
"shareInviteByHandle": "Handle",
"@shareInviteByHandle": {"description": "Inviting someone by their @handle, a choice in the invite dialog"},
"shareInviteByEmail": "Email",
"@shareInviteByEmail": {"description": "Inviting someone by email address, a choice in the invite dialog"},
"shareInviteAddress": "Email address",
"@shareInviteAddress": {"description": "Label of the invite dialog's address field"},
"shareInviteEmailHint": "They get a link by mail. Only whoever reads that mailbox can join, and the instance keeps the address only as a fingerprint.",
"@shareInviteEmailHint": {"description": "Below the address field of the invite dialog"},
"shareInviteMailed": "Invitation mailed to {address}",
"@shareInviteMailed": {"description": "Snack bar once an invitation by email went out", "placeholders": {"address": {"type": "String"}}},
"shareInvitedByEmail": "Invited by email",
"@shareInvitedByEmail": {"description": "A member row's name for an invitation mailed to an address, which members never see"},
"shareLinkTitle": "Join by link",
"@shareLinkTitle": {"description": "Switch on the Share sheet letting people join by the studyset's link"},
"shareLinkHint": "Anyone with the link can join as a Viewer. Switching it off ends the link for good.",
"@shareLinkHint": {"description": "Below the switch for joining by link"},
"shareLinkCopy": "Copy link",
"@shareLinkCopy": {"description": "Button copying the studyset's join link"},
"shareLinkCopied": "Link copied",
"@shareLinkCopied": {"description": "Snack bar once the join link is copied"},
"shareRefusedNotAnAddress": "That isn't an email address.",
"@shareRefusedNotAnAddress": {"description": "An invitation or code refused for an address that isn't one"},
"shareRefusedTooManyMails": "Too many mails to that address for now. Try again in an hour.",
"@shareRefusedTooManyMails": {"description": "An invitation or code refused as too many went out this hour"},
"shareRefusedMailFailed": "The mail couldn't be sent. Try again later.",
"@shareRefusedMailFailed": {"description": "An invitation or code whose mail failed"},
"shareRefusedHandleNeeded": "An invitation by mail names you by your handle: choose one in your profile first.",
"@shareRefusedHandleNeeded": {"description": "An invitation by email refused as the person has no handle"},
"libraryJoin": "Join with a link",
"@libraryJoin": {"description": "The Library's menu entry joining a studyset shared by link or invitation"},
"joinLinkLabel": "Link or code",
"@joinLinkLabel": {"description": "Field of the Join sheet taking a pasted link"},
"joinContinue": "Continue",
"@joinContinue": {"description": "Button looking up a pasted join link"},
"joinInvites": "{inviter} wants to share a studyset with you",
"@joinInvites": {"description": "What a join link offers", "placeholders": {"inviter": {"type": "String"}}},
"joinSomeone": "Someone",
"@joinSomeone": {"description": "Who invites, where they have no public profile"},
"joinAsViewer": "You can view and study it.",
"@joinAsViewer": {"description": "What joining as a Viewer gives"},
"joinAsEditor": "You can view, study and change it.",
"@joinAsEditor": {"description": "What joining as an Editor gives"},
"joinEmailHint": "This invitation went to an email address. Type it, and you'll get a code there that shows it's yours.",
"@joinEmailHint": {"description": "Above the address field of an invitation by email"},
"joinSendCode": "Send code",
"@joinSendCode": {"description": "Button mailing a code to accept an invitation"},
"joinCodeSent": "A code is on its way to {address}.",
"@joinCodeSent": {"description": "Once a code to join was mailed", "placeholders": {"address": {"type": "String"}}},
"joinCodeLabel": "Code from the mail",
"@joinCodeLabel": {"description": "Field taking the mailed code"},
"joinAction": "Join",
"@joinAction": {"description": "Button joining the shared studyset"},
"joinDone": "You joined. The studyset arrives in your Library with the next sync.",
"@joinDone": {"description": "Once joined"},
"joinAlreadyMember": "You're a member already.",
"@joinAlreadyMember": {"description": "A join link to a studyset the person is in already"},
"joinNotFound": "This link holds no more: it was switched off, has lapsed or was used already.",
"@joinNotFound": {"description": "A join link that holds no more"},
"joinWrongCode": "That code isn't right.",
"@joinWrongCode": {"description": "A mailed code typed wrong"},
"joinCodeExpired": "This code is no longer valid. Send a new one.",
"@joinCodeExpired": {"description": "A mailed code used, tried too often or too old"},
"joinOtherAddress": "This invitation went to another address.",
"@joinOtherAddress": {"description": "A code proving another address than the one invited"},
"inboxShared": "Shared with you",
"@inboxShared": {"description": "Heading of the Inbox's studysets shared with the person"},
"inboxSharedBy": "@{handle} shared {title} with you",
"@inboxSharedBy": {"description": "An Inbox entry of a studyset shared with the person", "placeholders": {"handle": {"type": "String"}, "title": {"type": "String"}}},
"inboxSharedNoHandle": "{title} was shared with you",
"@inboxSharedNoHandle": {"description": "An Inbox entry of a studyset shared by someone without a public profile", "placeholders": {"title": {"type": "String"}}},
"inboxSharedArriving": "A studyset on its way",
"@inboxSharedArriving": {"description": "The title of a studyset shared with the person that hasn't synced yet"},
"inboxSharedOpen": "Open",
"@inboxSharedOpen": {"description": "Button opening a studyset shared with the person"},
"studysetNew": "New",
"@studysetNew": {"description": "Mark on a library card of a studyset someone shared with the person, until opened"}
}