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:
parent
554e1604b4
commit
fb6b78fa19
1 changed files with 2443 additions and 54 deletions
|
|
@ -34,7 +34,13 @@ void main() {
|
|||
expect(invited.handle, 'Bob');
|
||||
expect(invited.role, SpaceRole.viewer);
|
||||
expect(await bob.mine(), [
|
||||
(id: space, kind: 'studyset', role: SpaceRole.viewer),
|
||||
(
|
||||
id: space,
|
||||
kind: 'studyset',
|
||||
role: SpaceRole.viewer,
|
||||
isNew: true,
|
||||
sharedBy: 'alice',
|
||||
),
|
||||
]);
|
||||
expect(keys.as('bob').keysNeeded(space), completion(isEmpty));
|
||||
|
||||
|
|
@ -96,4 +102,84 @@ void main() {
|
|||
expect((await bob.mine()).single.role, SpaceRole.owner);
|
||||
},
|
||||
);
|
||||
|
||||
test('an invitation mailed admits only who proves its address, as its '
|
||||
'role; its members see it by email alone', () async {
|
||||
final invited = await alice.inviteByEmail(
|
||||
space,
|
||||
'Dana@example.org',
|
||||
SpaceRole.editor,
|
||||
'en',
|
||||
);
|
||||
expect(invited.byEmail, isTrue);
|
||||
expect(invited.handle, isNull);
|
||||
final token = server.mailed.single.token;
|
||||
await expectLater(
|
||||
bob.inviteByEmail(space, 'erin@example.org', SpaceRole.viewer, 'en'),
|
||||
refused(SpaceRefusal.notFound),
|
||||
);
|
||||
|
||||
final preview = await bob.preview(token);
|
||||
expect(preview.byEmail, isTrue);
|
||||
expect(preview.role, SpaceRole.editor);
|
||||
expect(preview.inviterHandle, 'alice');
|
||||
|
||||
final other = await bob.sendJoinCode('bob@example.org', language: 'en');
|
||||
await expectLater(
|
||||
bob.acceptEmailInvitation(
|
||||
token,
|
||||
request: other,
|
||||
code: server.codeFor('bob@example.org'),
|
||||
),
|
||||
refused(SpaceRefusal.otherAddress),
|
||||
);
|
||||
final right = await bob.sendJoinCode('dana@example.org', language: 'en');
|
||||
await expectLater(
|
||||
bob.acceptEmailInvitation(token, request: right, code: 'WRONG'),
|
||||
refused(SpaceRefusal.wrongCode),
|
||||
);
|
||||
final joined = await bob.acceptEmailInvitation(
|
||||
token,
|
||||
request: right,
|
||||
code: server.codeFor('dana@example.org'),
|
||||
);
|
||||
expect(joined.role, SpaceRole.editor);
|
||||
expect(joined.isNew, isFalse);
|
||||
await expectLater(bob.preview(token), refused(SpaceRefusal.notFound));
|
||||
});
|
||||
|
||||
test(
|
||||
"a space's link admits whoever opens it as a Viewer while it is on",
|
||||
() async {
|
||||
await expectLater(
|
||||
bob.setJoinLink(space, on: true),
|
||||
refused(SpaceRefusal.notFound),
|
||||
);
|
||||
final link = (await alice.setJoinLink(space, on: true))!;
|
||||
final token = joinTokenOf(link)!;
|
||||
expect((await bob.preview(token)).role, SpaceRole.viewer);
|
||||
expect((await bob.joinByLink(token)).role, SpaceRole.viewer);
|
||||
|
||||
await alice.setJoinLink(space, on: false);
|
||||
await expectLater(
|
||||
server.as('carol').joinByLink(token),
|
||||
refused(SpaceRefusal.notFound),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('a space shared with someone is new to them until seen', () async {
|
||||
await alice.invite(space, 'bob', SpaceRole.editor);
|
||||
expect((await alice.mine()).single.isNew, isFalse);
|
||||
expect((await bob.mine()).single.sharedBy, 'alice');
|
||||
await bob.markSeen(space);
|
||||
expect((await bob.mine()).single.isNew, isFalse);
|
||||
});
|
||||
|
||||
test('a pasted link or token names the token', () {
|
||||
expect(joinTokenOf('https://learn.halkyon.eu/join/abc_D-9'), 'abc_D-9');
|
||||
expect(joinTokenOf(' learn.halkyon.eu/join/abc?utm=1 '), 'abc');
|
||||
expect(joinTokenOf('abc\n'), 'abc');
|
||||
expect(joinTokenOf(' '), isNull);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue