Changing the profile meant a Save button at the foot of the form, which the floating navigation bar half covered on a phone. A field now saves when it is left or Enter is pressed in it, and a link when it is removed; "Saving…" and "Saved" show beside the title. The form keeps what is being written as saves come back, where it used to start afresh and lose focus after each one. The handle is chosen in a dialog that asks the instance while it is typed whether it is free (`ProfileEndpoint.checkHandle`, which tells nothing `byHandle` doesn't), and choosing the first makes the profile with what was written. Saves go one at a time, the latest of those waiting next, and a handle chosen holds for every save after it, so a field's save can't take it back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
131 lines
3.4 KiB
Dart
131 lines
3.4 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
|
|
|
|
/// A [ProfileGateway] in memory, on an instance whose account domain is
|
|
/// `halkyon.eu`: one profile, refusing the handles others have (whatever
|
|
/// their case) and, once its handle is fixed, any other handle.
|
|
class FakeProfileGateway({
|
|
/// The person's profile; null while they have none.
|
|
var PublicProfile? profile,
|
|
|
|
/// Handles others have.
|
|
final Set<String> taken = const {},
|
|
}) implements ProfileGateway {
|
|
/// When set, every request fails with it.
|
|
Exception? fail;
|
|
|
|
@override
|
|
Future<PublicProfile?> mine() async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
return profile;
|
|
}
|
|
|
|
/// The handles checked, in order.
|
|
final checked = <String>[];
|
|
|
|
@override
|
|
Future<ProfileProblem?> checkHandle(String handle) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
checked.add(handle);
|
|
return _problemOf(handle);
|
|
}
|
|
|
|
ProfileProblem? _problemOf(String handle) {
|
|
final current = profile;
|
|
if (!RegExp(r'^[A-Za-z0-9_]{1,30}$').hasMatch(handle)) {
|
|
return ProfileProblem.handleInvalid;
|
|
}
|
|
if (handle.toLowerCase() == 'admin') {
|
|
return ProfileProblem.handleReserved;
|
|
}
|
|
if (taken.any((other) => other.toLowerCase() == handle.toLowerCase())) {
|
|
return ProfileProblem.handleTaken;
|
|
}
|
|
if (current != null &&
|
|
current.handleFixed &&
|
|
current.handle.toLowerCase() != handle.toLowerCase()) {
|
|
return ProfileProblem.published;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Every profile saved, in order.
|
|
final saves = <PublicProfile>[];
|
|
|
|
@override
|
|
Future<PublicProfile> save({
|
|
required String handle,
|
|
required String displayName,
|
|
required String bio,
|
|
required List<String> links,
|
|
}) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
final current = profile;
|
|
if (_problemOf(handle) case final problem?) {
|
|
throw ProfileRefused(problem);
|
|
}
|
|
final saved = profile = PublicProfile(
|
|
handle: handle,
|
|
address: '$handle@halkyon.eu',
|
|
displayName: displayName,
|
|
bio: bio,
|
|
links: links,
|
|
handleFixed: current?.handleFixed ?? false,
|
|
);
|
|
saves.add(saved);
|
|
return saved;
|
|
}
|
|
|
|
/// The avatars set, in order.
|
|
final avatars = <Uint8List>[];
|
|
|
|
@override
|
|
Future<PublicProfile> setAvatar(Uint8List png) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
avatars.add(png);
|
|
final current = profile!;
|
|
return profile = PublicProfile(
|
|
handle: current.handle,
|
|
address: current.address,
|
|
displayName: current.displayName,
|
|
bio: current.bio,
|
|
links: current.links,
|
|
avatarUrl: 'https://learn.halkyon.eu/avatars/${avatars.length}.png',
|
|
handleFixed: current.handleFixed,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<PublicProfile> removeAvatar() async {
|
|
final current = profile!;
|
|
return profile = PublicProfile(
|
|
handle: current.handle,
|
|
address: current.address,
|
|
displayName: current.displayName,
|
|
bio: current.bio,
|
|
links: current.links,
|
|
handleFixed: current.handleFixed,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> remove() async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
if (profile?.handleFixed ?? false) {
|
|
throw ProfileRefused(ProfileProblem.published);
|
|
}
|
|
profile = null;
|
|
}
|
|
}
|