perf(learn): discussions and feedback show what the person does at once

In a discussion, accepting an answer (or none), hiding or showing a post
and deleting one waited for the instance with every action disabled
(busy). DiscussionBloc now shows the discussion as the action leaves it
at once (Discussion.withAccepted, withPost, DiscussionPost.copyWith), the
actions staying available, and puts it back as it was where the instance
refuses (the refusal's snack bar as before) or can't be reached, then
reads it anew. Answering, commenting and editing still wait, as what
the instance makes of them is theirs to say.

In the feedback inbox, a thread opened shows read at once, a failure to
record it said to nobody (the next read shows the truth), and a decision
(closing, reopening, hiding, showing, declining) shows at once and is
put back with the failure's snack bar where it isn't written
(FeedbackThread.seen, decided). Accepting a suggestion still waits, as
it changes the author's content and can fail with SuggestionNotTaken.

Whats-New: Accepting, hiding and deleting in discussions, and closing or declining feedback, show at once.
Whats-New-De: Annehmen, Ausblenden und Löschen in Diskussionen sowie Schließen oder Ablehnen von Rückmeldungen zeigen sich sofort.
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-07 23:13:42 +01:00
commit 107e6f9913
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_editor/halkyon_editor.dart';
import 'package:halkyon_learn_flutter/src/core/profile/public_person.dart';
@ -287,6 +289,86 @@ void main() {
);
});
group('accepting, hiding and deleting show at once', () {
const bob = PublicPerson(handle: 'bob');
late FakeDiscussionGateway gateway;
late DiscussionBloc bloc;
final answer = DiscussionPost(
id: 'a1',
kind: DiscussionPostKind.answer,
createdAt: DateTime.utc(2026, 10, 16),
by: const PublicPerson(handle: 'ann'),
content: RichContent.plain('Topic marker.'),
);
Future<void> outcome(DiscussionOutcome outcome) => expectLater(
bloc.stream,
emitsThrough(
isA<DiscussionState>().having(
(state) => state.outcome,
'outcome',
outcome,
),
),
);
setUp(() async {
gateway = FakeDiscussionGateway(me: bob, author: true);
final question = gateway
.addQuestion('Meaning of は', answers: [answer])
.question
.id;
bloc = blocOf(gateway, question)..add(const DiscussionEventLoaded());
await expectLater(
bloc.stream,
emitsThrough(
isA<DiscussionState>().having(
(state) => state.status,
'status',
DiscussionStatus.ready,
),
),
);
});
test(
'accepted before the instance answers, nothing waiting on it',
() async {
final hold = gateway.hold = Completer<void>();
bloc.add(const DiscussionEventAccepted('a1'));
await pumpEventQueue();
expect(bloc.state.discussion!.acceptedAnswerId, 'a1');
expect(bloc.state.busy, isFalse);
hold.complete();
await outcome(DiscussionOutcome.accepted);
expect(bloc.state.discussion!.acceptedAnswerId, 'a1');
},
);
test('the instance out of reach, an answer accepted, a post hidden or '
'deleted is as it was again, and the person told', () async {
final hold = gateway.hold = Completer<void>();
gateway.fail = Exception('offline');
bloc.add(const DiscussionEventAccepted('a1'));
await pumpEventQueue();
expect(bloc.state.discussion!.acceptedAnswerId, 'a1');
hold.complete();
await outcome(DiscussionOutcome.failed);
expect(bloc.state.discussion!.acceptedAnswerId, isNull);
bloc.add(const DiscussionEventHidden('a1', hidden: true));
await outcome(DiscussionOutcome.failed);
expect(bloc.state.discussion!.answers.single.hidden, isFalse);
bloc.add(const DiscussionEventDeleted('a1'));
await outcome(DiscussionOutcome.failed);
expect(bloc.state.discussion!.answers.single.deleted, isFalse);
});
});
test('a discussion gone or not the person’s to see is missing', () async {
final bloc = blocOf(FakeDiscussionGateway(), 'nothing')
..add(const DiscussionEventLoaded());