feat(base)!: removing a profile makes it private (ADR 0020, D27)
Every account keeps a handle now, so removing a public profile no longer deletes it and frees the handle: it takes away what the profile shows and leaves it private, as a profile is until its person goes public (D5). - `profile.remove` and the register's `removeProfile` (members ask the register) empty the display name, bio, links with their checks and the avatar, keep the row with its handle, and return what is left, null for none. `/@handle` and `profiles.byHandle` then answer as for a handle nobody has. Once the person has published, removing is refused as before. - Learn: the profile form starts afresh from what is left, "Remove profile" shows only while something is filled in, and the dialog says the handle stays. - The module's README and ADR 0020 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
630965d920
commit
d5651c5beb
24 changed files with 255 additions and 60 deletions
|
|
@ -514,7 +514,7 @@
|
||||||
"publicProfileHandleConfirmChange": "Ändern",
|
"publicProfileHandleConfirmChange": "Ändern",
|
||||||
"publicProfileRemove": "Profil entfernen",
|
"publicProfileRemove": "Profil entfernen",
|
||||||
"publicProfileRemoveTitle": "Dein öffentliches Profil entfernen?",
|
"publicProfileRemoveTitle": "Dein öffentliches Profil entfernen?",
|
||||||
"publicProfileRemoveMessage": "Dein Handle wird für andere frei.",
|
"publicProfileRemoveMessage": "Anzeigename, Text über dich, Links und Bild werden entfernt. Dein Handle bleibt deins und privat, bis du dein Profil wieder ausfüllst oder etwas veröffentlichst.",
|
||||||
"publicProfileRemoved": "Profil entfernt.",
|
"publicProfileRemoved": "Profil entfernt.",
|
||||||
"publicProfileFailed": "Die Instanz ist nicht erreichbar. Versuche es erneut.",
|
"publicProfileFailed": "Die Instanz ist nicht erreichbar. Versuche es erneut.",
|
||||||
"publicProfileUnreachable": "Dein öffentliches Profil ist gerade nicht erreichbar.",
|
"publicProfileUnreachable": "Dein öffentliches Profil ist gerade nicht erreichbar.",
|
||||||
|
|
|
||||||
|
|
@ -585,7 +585,7 @@
|
||||||
"publicProfileHandleConfirmChange": "Change",
|
"publicProfileHandleConfirmChange": "Change",
|
||||||
"publicProfileRemove": "Remove profile",
|
"publicProfileRemove": "Remove profile",
|
||||||
"publicProfileRemoveTitle": "Remove your public profile?",
|
"publicProfileRemoveTitle": "Remove your public profile?",
|
||||||
"publicProfileRemoveMessage": "Your handle becomes free for others to take.",
|
"publicProfileRemoveMessage": "Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.",
|
||||||
"publicProfileRemoved": "Profile removed.",
|
"publicProfileRemoved": "Profile removed.",
|
||||||
"publicProfileFailed": "Couldn't reach the instance. Try again.",
|
"publicProfileFailed": "Couldn't reach the instance. Try again.",
|
||||||
"publicProfileUnreachable": "Your public profile can't be reached right now.",
|
"publicProfileUnreachable": "Your public profile can't be reached right now.",
|
||||||
|
|
|
||||||
|
|
@ -3085,7 +3085,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @publicProfileRemoveMessage.
|
/// No description provided for @publicProfileRemoveMessage.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Your handle becomes free for others to take.'**
|
/// **'Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.'**
|
||||||
String get publicProfileRemoveMessage;
|
String get publicProfileRemoveMessage;
|
||||||
|
|
||||||
/// No description provided for @publicProfileRemoved.
|
/// No description provided for @publicProfileRemoved.
|
||||||
|
|
|
||||||
|
|
@ -1869,7 +1869,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get publicProfileRemoveTitle => 'Dein öffentliches Profil entfernen?';
|
String get publicProfileRemoveTitle => 'Dein öffentliches Profil entfernen?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileRemoveMessage => 'Dein Handle wird für andere frei.';
|
String get publicProfileRemoveMessage =>
|
||||||
|
'Anzeigename, Text über dich, Links und Bild werden entfernt. Dein Handle bleibt deins und privat, bis du dein Profil wieder ausfüllst oder etwas veröffentlichst.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileRemoved => 'Profil entfernt.';
|
String get publicProfileRemoved => 'Profil entfernt.';
|
||||||
|
|
|
||||||
|
|
@ -1852,7 +1852,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileRemoveMessage =>
|
String get publicProfileRemoveMessage =>
|
||||||
'Your handle becomes free for others to take.';
|
'Your display name, bio, links and picture are removed. Your handle stays yours and private until you fill in your profile again or publish something.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileRemoved => 'Profile removed.';
|
String get publicProfileRemoved => 'Profile removed.';
|
||||||
|
|
|
||||||
|
|
@ -216,9 +216,12 @@ class PublicProfileBloc(final ProfileRepository _profiles)
|
||||||
if (state case PublicProfileStateReady(:final profile?, saving: false)) {
|
if (state case PublicProfileStateReady(:final profile?, saving: false)) {
|
||||||
emit(PublicProfileStateReady(profile: profile, saving: true));
|
emit(PublicProfileStateReady(profile: profile, saving: true));
|
||||||
try {
|
try {
|
||||||
await _profiles.remove();
|
final left = await _profiles.remove();
|
||||||
emit(
|
emit(
|
||||||
const PublicProfileStateReady(outcome: PublicProfileOutcome.removed),
|
PublicProfileStateReady(
|
||||||
|
profile: left,
|
||||||
|
outcome: PublicProfileOutcome.removed,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} on ProfileRefused catch (refused) {
|
} on ProfileRefused catch (refused) {
|
||||||
emit(
|
emit(
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,10 @@ abstract interface class ProfileGateway() {
|
||||||
/// when it can.
|
/// when it can.
|
||||||
Future<ProfileProblem?> checkHandle(String handle);
|
Future<ProfileProblem?> checkHandle(String handle);
|
||||||
|
|
||||||
/// Removes the person's public profile. Throws [ProfileRefused] once they
|
/// Removes what the person's public profile shows, making it private
|
||||||
/// have published.
|
/// again with its handle (ADR 0020, D27); returns what is left, null while
|
||||||
Future<void> remove();
|
/// they have none. Throws [ProfileRefused] once they have published.
|
||||||
|
Future<PublicProfile?> remove();
|
||||||
|
|
||||||
/// Shows [png] as the person's avatar; returns the profile with it.
|
/// Shows [png] as the person's avatar; returns the profile with it.
|
||||||
/// Throws [ProfileRefused] for anything but a PNG of at most 1 MB.
|
/// Throws [ProfileRefused] for anything but a PNG of at most 1 MB.
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,10 @@ class ProfileRepository(
|
||||||
Future<ProfileProblem?> checkHandle(String handle) =>
|
Future<ProfileProblem?> checkHandle(String handle) =>
|
||||||
_gateway.checkHandle(handle.trim());
|
_gateway.checkHandle(handle.trim());
|
||||||
|
|
||||||
/// Removes the person's public profile. Throws [ProfileRefused] once they
|
/// Removes what the person's public profile shows, making it private
|
||||||
/// have published.
|
/// again with its handle (ADR 0020, D27); returns what is left, null while
|
||||||
Future<void> remove() => _gateway.remove();
|
/// they have none. Throws [ProfileRefused] once they have published.
|
||||||
|
Future<PublicProfile?> remove() => _gateway.remove();
|
||||||
|
|
||||||
/// Shows [picture] (a PNG, JPEG, WebP or GIF) as the person's avatar,
|
/// Shows [picture] (a PNG, JPEG, WebP or GIF) as the person's avatar,
|
||||||
/// drawn anew as a square PNG ([avatarPng]): metadata such as where a
|
/// drawn anew as a square PNG ([avatarPng]): metadata such as where a
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,12 @@ class ServerpodProfileGateway(final protocol.Client _client)
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> remove() => _refusing(_profiles.remove);
|
Future<PublicProfile?> remove() => _refusing(
|
||||||
|
() async => switch (await _profiles.remove()) {
|
||||||
|
final profile? => _profile(profile),
|
||||||
|
null => null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PublicProfile> setAvatar(Uint8List png) => _refusing(
|
Future<PublicProfile> setAvatar(Uint8List png) => _refusing(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The person's public profile (ADR 0005, rule 2), in the Profile tab: what
|
/// The person's public profile (ADR 0005, rule 2), in the Profile tab: what
|
||||||
/// it is for, and a form of its handle, display name, bio and links that
|
/// it is for, and a form of its handle, display name, bio and links that
|
||||||
/// saves as it is written; a way to remove it until the person publishes.
|
/// saves as it is written; a way to remove what it shows, making it private
|
||||||
|
/// with its handle (ADR 0020, D27), until the person publishes.
|
||||||
class const PublicProfileSection({super.key}) extends StatelessWidget {
|
class const PublicProfileSection({super.key}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => BlocProvider(
|
Widget build(BuildContext context) => BlocProvider(
|
||||||
|
|
@ -23,7 +24,16 @@ class const PublicProfileSection({super.key}) extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class const _Section() extends StatelessWidget {
|
class const _Section() extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<_Section> createState() => _SectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionState() extends State<_Section> {
|
||||||
|
/// How often the profile was removed here: the form starts afresh each
|
||||||
|
/// time, from what is left.
|
||||||
|
var _removals = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
@ -35,6 +45,11 @@ class const _Section() extends StatelessWidget {
|
||||||
now.outcome != null &&
|
now.outcome != null &&
|
||||||
now.outcome != PublicProfileOutcome.saved,
|
now.outcome != PublicProfileOutcome.saved,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
if (state case PublicProfileStateReady(
|
||||||
|
outcome: PublicProfileOutcome.removed,
|
||||||
|
)) {
|
||||||
|
setState(() => _removals++);
|
||||||
|
}
|
||||||
final message = switch (state) {
|
final message = switch (state) {
|
||||||
PublicProfileStateReady(outcome: PublicProfileOutcome.removed) =>
|
PublicProfileStateReady(outcome: PublicProfileOutcome.removed) =>
|
||||||
l10n.publicProfileRemoved,
|
l10n.publicProfileRemoved,
|
||||||
|
|
@ -107,7 +122,7 @@ class const _Section() extends StatelessWidget {
|
||||||
_ProfileForm(
|
_ProfileForm(
|
||||||
// Kept while the person writes, as saves come back; afresh
|
// Kept while the person writes, as saves come back; afresh
|
||||||
// once a profile is made or removed.
|
// once a profile is made or removed.
|
||||||
key: ValueKey(profile != null),
|
key: ValueKey((profile != null, _removals)),
|
||||||
profile: profile,
|
profile: profile,
|
||||||
saving: saving,
|
saving: saving,
|
||||||
checkingLinks: checkingLinks,
|
checkingLinks: checkingLinks,
|
||||||
|
|
@ -272,6 +287,14 @@ class _ProfileFormState() extends State<_ProfileForm> {
|
||||||
_saveIfChanged();
|
_saveIfChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether [profile] shows anything removing would take away: with the
|
||||||
|
/// handle alone it is private already (ADR 0020, D5).
|
||||||
|
static bool _showsSomething(PublicProfile profile) =>
|
||||||
|
profile.displayName.isNotEmpty ||
|
||||||
|
profile.bio.isNotEmpty ||
|
||||||
|
profile.links.isNotEmpty ||
|
||||||
|
profile.avatarUrl != null;
|
||||||
|
|
||||||
Future<void> _remove() async {
|
Future<void> _remove() async {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final bloc = context.read<PublicProfileBloc>();
|
final bloc = context.read<PublicProfileBloc>();
|
||||||
|
|
@ -450,7 +473,7 @@ class _ProfileFormState() extends State<_ProfileForm> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (profile != null && !fixed) ...[
|
if (profile != null && !fixed && _showsSomething(profile)) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Align(
|
Align(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
|
|
||||||
|
|
@ -212,25 +212,56 @@ void main() {
|
||||||
expect(find.text('Remove profile'), findsNothing);
|
expect(find.text('Remove profile'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('removing a profile asks first, then frees its handle', (
|
testWidgets('removing a profile asks first, then makes it private: the '
|
||||||
tester,
|
'handle stays, what was filled in goes (ADR 0020, D27)', (tester) async {
|
||||||
) async {
|
|
||||||
final profiles = FakeProfileGateway(
|
final profiles = FakeProfileGateway(
|
||||||
profile: const PublicProfile(
|
profile: const PublicProfile(
|
||||||
handle: 'raisondetredev',
|
handle: 'raisondetredev',
|
||||||
address: 'raisondetredev@halkyon.eu',
|
address: 'raisondetredev@halkyon.eu',
|
||||||
|
displayName: 'Raison',
|
||||||
|
bio: 'Learning Japanese.',
|
||||||
|
links: ['https://halkyon.app'],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await showProfile(tester, profiles);
|
await showProfile(tester, profiles);
|
||||||
|
expect(find.widgetWithText(TextField, 'Raison'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Remove profile'));
|
await tester.tap(find.text('Remove profile'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Remove your public profile?'), findsOneWidget);
|
expect(find.text('Remove your public profile?'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Your handle stays yours'), findsOneWidget);
|
||||||
await tester.tap(find.text('Remove profile').last);
|
await tester.tap(find.text('Remove profile').last);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Profile removed.'), findsOneWidget);
|
expect(find.text('Profile removed.'), findsOneWidget);
|
||||||
expect(profiles.profile, isNull);
|
expect(profiles.profile?.handle, 'raisondetredev');
|
||||||
|
expect(profiles.profile?.displayName, '');
|
||||||
|
expect(find.text('@raisondetredev'), findsOneWidget);
|
||||||
|
// The form starts afresh from what is left.
|
||||||
|
expect(find.widgetWithText(TextField, 'Raison'), findsNothing);
|
||||||
|
expect(find.widgetWithText(TextField, 'Learning Japanese.'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.widgetWithText(TextField, 'https://halkyon.app'),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
// Nothing is left to remove.
|
||||||
|
expect(find.text('Remove profile'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('offers no removing while nothing is filled in: the handle '
|
||||||
|
'alone is private already', (tester) async {
|
||||||
|
await showProfile(
|
||||||
|
tester,
|
||||||
|
FakeProfileGateway(
|
||||||
|
profile: const PublicProfile(
|
||||||
|
handle: 'raisondetredev',
|
||||||
|
address: 'raisondetredev@halkyon.eu',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('@raisondetredev'), findsOneWidget);
|
||||||
|
expect(find.text('Remove profile'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('a saved profile takes a picture as its avatar, and gives it '
|
testWidgets('a saved profile takes a picture as its avatar, and gives it '
|
||||||
|
|
|
||||||
|
|
@ -145,14 +145,23 @@ class FakeProfileGateway({
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> remove() async {
|
Future<PublicProfile?> remove() async {
|
||||||
if (fail case final error?) {
|
if (fail case final error?) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (profile?.handleFixed ?? false) {
|
final current = profile;
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (current.handleFixed) {
|
||||||
throw ProfileRefused(ProfileProblem.published);
|
throw ProfileRefused(ProfileProblem.published);
|
||||||
}
|
}
|
||||||
profile = null;
|
// Private again, the handle kept (ADR 0020, D27).
|
||||||
|
return profile = PublicProfile(
|
||||||
|
handle: current.handle,
|
||||||
|
address: current.address,
|
||||||
|
pageUrl: current.pageUrl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The links whose pages link back to the profile page.
|
/// The links whose pages link back to the profile page.
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,10 @@
|
||||||
one `HandleField` with its `HandleCheckCubit` (asked 300 ms after typing pauses, only
|
one `HandleField` with its `HandleCheckCubit` (asked 300 ms after typing pauses, only
|
||||||
for the handle typed last, the instance's suggestion free without asking) serves the
|
for the handle typed last, the instance's suggestion free without asking) serves the
|
||||||
password setup's handle step, between the phrase check and the code, and
|
password setup's handle step, between the phrase check and the code, and
|
||||||
SimpleLogin's sign-up and keys steps.
|
SimpleLogin's sign-up and keys steps. Then D27: `profile.remove` (and the register's
|
||||||
|
`removeProfile`, members asking it) empties the display name, bio, links and their
|
||||||
|
checks and the avatar, keeps the row with its handle, and returns what is left; Learn's
|
||||||
|
profile form starts afresh from it and offers no removing while nothing is filled in.
|
||||||
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
||||||
register), ADR 0017 (a password, TOTP and the keys).
|
register), ADR 0017 (a password, TOTP and the keys).
|
||||||
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
||||||
|
|
|
||||||
|
|
@ -835,14 +835,17 @@ class EndpointProfile extends _isc.EndpointRef {
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Removes your public profile, freeing its handle; nothing when you have
|
/// Removes what your public profile shows (display name, bio, links,
|
||||||
/// none. Throws [ProfileRefusedException] once your handle is fixed:
|
/// avatar), making it private again: the handle stays yours (Halkyon
|
||||||
/// others refer to it.
|
/// Learn's ADR 0020, D27). Returns it; null when you have none. Throws
|
||||||
_ida.Future<void> remove() => caller.callServerEndpoint<void>(
|
/// [ProfileRefusedException] once your handle is fixed: others refer to
|
||||||
'halkyon_base.profile',
|
/// it.
|
||||||
'remove',
|
_ida.Future<_i8qdmv6n.Profile?> remove() =>
|
||||||
{},
|
caller.callServerEndpoint<_i8qdmv6n.Profile?>(
|
||||||
);
|
'halkyon_base.profile',
|
||||||
|
'remove',
|
||||||
|
{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The public profiles of others, found by their handles. Any signed-in
|
/// The public profiles of others, found by their handles. Any signed-in
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
||||||
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
||||||
| `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
| `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
||||||
| `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone |
|
| `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone |
|
||||||
| `profile` | `mine`, `save`, `checkHandle` (with every answer about a new handle, at most 30 an hour per person and 60 per network), `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` |
|
| `profile` | `mine`, `save`, `checkHandle` (with every answer about a new handle, at most 30 an hour per person and 60 per network), `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove` (what it shows goes, the handle stays, private again: Halkyon Learn's ADR 0020, D27): the caller's own public profile | `identified` |
|
||||||
| `profiles` | `byHandle`: anyone's public profile once they went public, whatever the handle's case | signed in |
|
| `profiles` | `byHandle`: anyone's public profile once they went public, whatever the handle's case | signed in |
|
||||||
|
|
||||||
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
|
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,12 @@ class ProfileEndpoint extends Endpoint {
|
||||||
Future<Profile> removeAvatar(Session session) =>
|
Future<Profile> removeAvatar(Session session) =>
|
||||||
_register(session).removeAvatar(session, _caller(session));
|
_register(session).removeAvatar(session, _caller(session));
|
||||||
|
|
||||||
/// Removes your public profile, freeing its handle; nothing when you have
|
/// Removes what your public profile shows (display name, bio, links,
|
||||||
/// none. Throws [ProfileRefusedException] once your handle is fixed:
|
/// avatar), making it private again: the handle stays yours (Halkyon
|
||||||
/// others refer to it.
|
/// Learn's ADR 0020, D27). Returns it; null when you have none. Throws
|
||||||
Future<void> remove(Session session) =>
|
/// [ProfileRefusedException] once your handle is fixed: others refer to
|
||||||
|
/// it.
|
||||||
|
Future<Profile?> remove(Session session) =>
|
||||||
_register(session).removeProfile(session, _caller(session));
|
_register(session).removeProfile(session, _caller(session));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,9 +102,11 @@ abstract interface class AccountRegister {
|
||||||
/// `ProfileNotFoundException` while they have no profile.
|
/// `ProfileNotFoundException` while they have no profile.
|
||||||
Future<Profile> removeAvatar(Session session, UuidValue person);
|
Future<Profile> removeAvatar(Session session, UuidValue person);
|
||||||
|
|
||||||
/// Removes [person]'s public profile, freeing its handle; nothing when they
|
/// Removes what [person]'s public profile shows (display name, bio, links,
|
||||||
/// have none. Throws `ProfileRefusedException` once their handle is fixed.
|
/// avatar), making it private again, the handle kept theirs (Halkyon
|
||||||
Future<void> removeProfile(Session session, UuidValue person);
|
/// Learn's ADR 0020, D27); returns it, null when they have none. Throws
|
||||||
|
/// `ProfileRefusedException` once their handle is fixed.
|
||||||
|
Future<Profile?> removeProfile(Session session, UuidValue person);
|
||||||
|
|
||||||
/// Fixes [person]'s handle, as others now refer to it (Halkyon Learn: their
|
/// Fixes [person]'s handle, as others now refer to it (Halkyon Learn: their
|
||||||
/// first publish); nothing when it is fixed already or they have no
|
/// first publish); nothing when it is fixed already or they have no
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,11 @@ Future<Map<String, Object?>?> _answer(
|
||||||
);
|
);
|
||||||
return {'profile': profile.toJson()};
|
return {'profile': profile.toJson()};
|
||||||
case RegisterOperation.removeProfile:
|
case RegisterOperation.removeProfile:
|
||||||
await register.removeProfile(session, arguments.id('person'));
|
final profile = await register.removeProfile(
|
||||||
return const {};
|
session,
|
||||||
|
arguments.id('person'),
|
||||||
|
);
|
||||||
|
return {'profile': profile?.toJson()};
|
||||||
case RegisterOperation.fixHandle:
|
case RegisterOperation.fixHandle:
|
||||||
await register.fixHandle(session, arguments.id('person'));
|
await register.fixHandle(session, arguments.id('person'));
|
||||||
return const {};
|
return const {};
|
||||||
|
|
|
||||||
|
|
@ -318,22 +318,37 @@ final class LocalRegister implements AccountRegister {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeProfile(Session session, UuidValue person) async {
|
Future<Profile?> removeProfile(Session session, UuidValue person) async {
|
||||||
final mine = await _rowOf(session, person);
|
final mine = await _rowOf(session, person);
|
||||||
if (mine == null) {
|
if (mine == null) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
if (mine.firstPublishedAt != null) {
|
if (mine.firstPublishedAt != null) {
|
||||||
throw ProfileRefusedException(reason: ProfileRefusal.published);
|
throw ProfileRefusedException(reason: ProfileRefusal.published);
|
||||||
}
|
}
|
||||||
// DELETE FROM "public_profile" WHERE "authUserId" = $1
|
// UPDATE "public_profile" SET "displayName" = '', "bio" = '',
|
||||||
await PublicProfile.db.deleteWhere(
|
// "links" = '[]', "verifiedLinks" = NULL, "avatarHash" = NULL,
|
||||||
|
// "updatedAt" = $1 WHERE "id" = $2
|
||||||
|
final emptied = await PublicProfile.db.updateRow(
|
||||||
session,
|
session,
|
||||||
where: (t) => t.authUserId.equals(person),
|
mine.copyWith(
|
||||||
|
displayName: '',
|
||||||
|
bio: '',
|
||||||
|
links: const [],
|
||||||
|
// Not the default: the generated copyWith tells an explicit null
|
||||||
|
// (clear it) from no argument (keep it), which the lint can't see.
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
verifiedLinks: null,
|
||||||
|
// As above.
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
avatarHash: null,
|
||||||
|
updatedAt: clock.now().toUtc(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (mine.avatarHash case final hash?) {
|
if (mine.avatarHash case final hash?) {
|
||||||
await forgetAvatar(session, hash);
|
await forgetAvatar(session, hash);
|
||||||
}
|
}
|
||||||
|
return await profileViewOf(session, emptied);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,12 @@ final class RemoteRegister implements AccountRegister {
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeProfile(Session session, UuidValue person) =>
|
Future<Profile?> removeProfile(Session session, UuidValue person) async =>
|
||||||
_ask(RegisterOperation.removeProfile, {'person': person.uuid});
|
_profile(
|
||||||
|
(await _ask(RegisterOperation.removeProfile, {
|
||||||
|
'person': person.uuid,
|
||||||
|
}))['profile'],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> fixHandle(Session session, UuidValue person) =>
|
Future<void> fixHandle(Session session, UuidValue person) =>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue