added guardian letters with chat and multiple answer functionalities
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_exception.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
import '../../../../../utils/random_id.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/parent_letter_repository.dart';
|
||||
import 'parent_letter_event.dart';
|
||||
import 'parent_letter_state.dart';
|
||||
import 'parent_letters_bloc.dart';
|
||||
|
||||
/// One letter; [id] is the letter id so each letter keeps its own hydrated
|
||||
/// cache entry. Changes are mirrored into the app-wide [inbox].
|
||||
class ParentLetterBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
ParentLetterEvent,
|
||||
ParentLetterState,
|
||||
ParentLetterRepository
|
||||
> {
|
||||
final String letterId;
|
||||
final ParentLettersBloc inbox;
|
||||
|
||||
ParentLetterBloc(this.letterId, {required this.inbox});
|
||||
|
||||
@override
|
||||
String get id => letterId;
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final ParentLetterDetail letter;
|
||||
try {
|
||||
letter = await repo.getLetter(letterId);
|
||||
} on ParentLetterException catch (e) {
|
||||
if (e.error == ParentLetterError.letterNotFound) {
|
||||
// Withdrawn: a hydrated copy must not stay readable.
|
||||
add(Emit((state) => state.copyWith(letter: null)));
|
||||
unawaited(inbox.refresh(silent: true));
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
add(DataGathered((state) => state.copyWith(letter: letter)));
|
||||
if (!letter.summary.read) inbox.markRead(letterId);
|
||||
}
|
||||
|
||||
Future<void> submitResponse({
|
||||
required String childId,
|
||||
required List<ParentLetterAnswer> answers,
|
||||
Uint8List? signaturePng,
|
||||
}) async {
|
||||
final letter = await repo.submitResponse(
|
||||
letterId: letterId,
|
||||
childId: childId,
|
||||
answers: answers,
|
||||
signaturePng: signaturePng,
|
||||
);
|
||||
if (isClosed) return;
|
||||
add(Emit((state) => state.copyWith(letter: letter)));
|
||||
inbox.applyDetail(letter);
|
||||
}
|
||||
|
||||
Future<void> sendThreadMessage(String body) async {
|
||||
final message = await repo.sendThreadMessage(
|
||||
letterId: letterId,
|
||||
body: body,
|
||||
clientMessageId: randomUuidV4(),
|
||||
);
|
||||
if (isClosed) return;
|
||||
add(
|
||||
Emit((state) {
|
||||
final letter = state.letter;
|
||||
if (letter == null) return state;
|
||||
final thread = letter.content.thread;
|
||||
return state.copyWith(
|
||||
letter: ParentLetterDetail(
|
||||
summary: letter.summary,
|
||||
content: letter.content.copyWith(
|
||||
thread: thread.copyWith(messages: [...thread.messages, message]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List> loadAttachment(String attachmentId) =>
|
||||
repo.getAttachment(letterId: letterId, attachmentId: attachmentId);
|
||||
|
||||
@override
|
||||
ParentLetterRepository repository() => ParentLetterRepository();
|
||||
|
||||
@override
|
||||
ParentLetterState fromNothing() => const ParentLetterState();
|
||||
|
||||
@override
|
||||
ParentLetterState fromStorage(Map<String, dynamic> json) =>
|
||||
ParentLetterState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(ParentLetterState state) => state.toJson();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'parent_letter_state.dart';
|
||||
|
||||
sealed class ParentLetterEvent
|
||||
extends LoadableHydratedBlocEvent<ParentLetterState> {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
|
||||
part 'parent_letter_state.freezed.dart';
|
||||
part 'parent_letter_state.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterState with _$ParentLetterState {
|
||||
const factory ParentLetterState({ParentLetterDetail? letter}) =
|
||||
_ParentLetterState;
|
||||
|
||||
factory ParentLetterState.fromJson(Map<String, dynamic> json) =>
|
||||
_$ParentLetterStateFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'parent_letter_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ParentLetterState {
|
||||
|
||||
ParentLetterDetail? get letter;
|
||||
/// Create a copy of ParentLetterState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ParentLetterStateCopyWith<ParentLetterState> get copyWith => _$ParentLetterStateCopyWithImpl<ParentLetterState>(this as ParentLetterState, _$identity);
|
||||
|
||||
/// Serializes this ParentLetterState to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
final _this = this as ParentLetterState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterState&&(identical(other.letter, _this.letter) || other.letter == _this.letter));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
final _this = this as ParentLetterState;
|
||||
return Object.hash(runtimeType,_this.letter);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final _this = this as ParentLetterState;
|
||||
return 'ParentLetterState(letter: ${_this.letter})';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ParentLetterStateCopyWith<$Res> {
|
||||
factory $ParentLetterStateCopyWith(ParentLetterState value, $Res Function(ParentLetterState) _then) = _$ParentLetterStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
ParentLetterDetail? letter
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ParentLetterStateCopyWithImpl<$Res>
|
||||
implements $ParentLetterStateCopyWith<$Res> {
|
||||
_$ParentLetterStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ParentLetterState _self;
|
||||
final $Res Function(ParentLetterState) _then;
|
||||
|
||||
/// Create a copy of ParentLetterState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? letter = freezed,}) {
|
||||
return _then(ParentLetterState(
|
||||
letter: freezed == letter ? _self.letter : letter // ignore: cast_nullable_to_non_nullable
|
||||
as ParentLetterDetail?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ParentLetterState].
|
||||
extension ParentLetterStatePatterns on ParentLetterState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ParentLetterState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ParentLetterState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ParentLetterState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( ParentLetterDetail? letter)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState() when $default != null:
|
||||
return $default(_that.letter);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( ParentLetterDetail? letter) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState():
|
||||
return $default(_that.letter);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( ParentLetterDetail? letter)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLetterState() when $default != null:
|
||||
return $default(_that.letter);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ParentLetterState implements ParentLetterState {
|
||||
const _ParentLetterState({this.letter});
|
||||
factory _ParentLetterState.fromJson(Map<String, dynamic> json) => _$ParentLetterStateFromJson(json);
|
||||
|
||||
@override final ParentLetterDetail? letter;
|
||||
|
||||
/// Create a copy of ParentLetterState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ParentLetterStateCopyWith<_ParentLetterState> get copyWith => __$ParentLetterStateCopyWithImpl<_ParentLetterState>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ParentLetterStateToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterState&&(identical(other.letter, letter) || other.letter == letter));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,letter);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ParentLetterState(letter: $letter)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ParentLetterStateCopyWith<$Res> implements $ParentLetterStateCopyWith<$Res> {
|
||||
factory _$ParentLetterStateCopyWith(_ParentLetterState value, $Res Function(_ParentLetterState) _then) = __$ParentLetterStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
ParentLetterDetail? letter
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ParentLetterStateCopyWithImpl<$Res>
|
||||
implements _$ParentLetterStateCopyWith<$Res> {
|
||||
__$ParentLetterStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ParentLetterState _self;
|
||||
final $Res Function(_ParentLetterState) _then;
|
||||
|
||||
/// Create a copy of ParentLetterState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? letter = freezed,}) {
|
||||
return _then(_ParentLetterState(
|
||||
letter: freezed == letter ? _self.letter : letter // ignore: cast_nullable_to_non_nullable
|
||||
as ParentLetterDetail?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'parent_letter_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ParentLetterState _$ParentLetterStateFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterState(
|
||||
letter: json['letter'] == null
|
||||
? null
|
||||
: ParentLetterDetail.fromJson(json['letter'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterStateToJson(_ParentLetterState instance) =>
|
||||
<String, dynamic>{'letter': instance.letter};
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import '../../../../../access/access_requirement.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../parent_letters_logic.dart';
|
||||
import '../repository/parent_letters_repository.dart';
|
||||
import 'parent_letters_event.dart';
|
||||
import 'parent_letters_state.dart';
|
||||
|
||||
/// The guardian's letter inbox. Lives app-wide because the module badge and
|
||||
/// push handling need it outside the page.
|
||||
class ParentLettersBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
ParentLettersEvent,
|
||||
ParentLettersState,
|
||||
ParentLettersRepository
|
||||
> {
|
||||
@override
|
||||
Set<AccessRequirement> get requirements => const {AccessRequirement.guardian};
|
||||
|
||||
Future<void>? _loading;
|
||||
|
||||
/// App start, prefetch, page visit and push handling all ask for the first
|
||||
/// page within moments of each other; they share one request.
|
||||
@override
|
||||
Future<void> gatherData() =>
|
||||
_loading ??= _loadFirstPage().whenComplete(() => _loading = null);
|
||||
|
||||
Future<void> _loadFirstPage() async {
|
||||
final page = await repo.getLetters();
|
||||
if (isClosed) return;
|
||||
add(DataGathered((_) => firstPageState(page)));
|
||||
}
|
||||
|
||||
/// Reloads the first page; older pages loaded via [loadOlder] are dropped.
|
||||
Future<void> refresh({bool silent = false}) async {
|
||||
if (!requirementsMet) return;
|
||||
if (!silent) add(RefetchStarted<ParentLettersState>());
|
||||
try {
|
||||
await gatherData();
|
||||
} catch (e) {
|
||||
if (isClosed) return;
|
||||
if (silent) {
|
||||
log('Silent parent letters refresh failed: $e');
|
||||
} else {
|
||||
addLoadingError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadOlder() async {
|
||||
final letters = innerState?.letters;
|
||||
if (letters == null || letters.isEmpty) return;
|
||||
final page = await repo.getLetters(before: letters.last.id);
|
||||
if (isClosed) return;
|
||||
add(Emit((state) => appendOlderPage(state, page)));
|
||||
}
|
||||
|
||||
/// Optimistic locally. When nothing could be applied locally (a letter
|
||||
/// opened from a push is not in the inbox yet), the follow-up refresh picks
|
||||
/// it up.
|
||||
void markRead(String letterId) {
|
||||
final current = innerState;
|
||||
final updated = current == null ? null : withLetterRead(current, letterId);
|
||||
final appliedLocally = updated != null && !identical(updated, current);
|
||||
if (appliedLocally) add(Emit((_) => updated));
|
||||
repo
|
||||
.markRead(letterId)
|
||||
.then((_) {
|
||||
if (!appliedLocally) _reloadAfterWrite();
|
||||
})
|
||||
.catchError((Object e) {
|
||||
log('Marking parent letter $letterId as read failed: $e');
|
||||
_reloadAfterWrite();
|
||||
});
|
||||
}
|
||||
|
||||
/// A request already in flight may predate the write, so it is not joined.
|
||||
Future<void> _reloadAfterWrite() async {
|
||||
try {
|
||||
await _loading;
|
||||
} on Object {
|
||||
// Its own caller reports the failure.
|
||||
}
|
||||
await refresh(silent: true);
|
||||
}
|
||||
|
||||
/// Takes over a freshly loaded letter.
|
||||
void applyDetail(ParentLetterDetail detail) =>
|
||||
add(Emit((state) => withSummary(state, detail.summary)));
|
||||
|
||||
@override
|
||||
ParentLettersRepository repository() => ParentLettersRepository();
|
||||
|
||||
@override
|
||||
ParentLettersState fromNothing() => const ParentLettersState();
|
||||
|
||||
@override
|
||||
ParentLettersState fromStorage(Map<String, dynamic> json) =>
|
||||
ParentLettersState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(ParentLettersState state) => state.toJson();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'parent_letters_state.dart';
|
||||
|
||||
sealed class ParentLettersEvent
|
||||
extends LoadableHydratedBlocEvent<ParentLettersState> {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
|
||||
part 'parent_letters_state.freezed.dart';
|
||||
part 'parent_letters_state.g.dart';
|
||||
|
||||
/// Hydrated inbox. [unreadCount] and [openCount] cover all letters on the
|
||||
/// server, not just the loaded [letters].
|
||||
@freezed
|
||||
abstract class ParentLettersState with _$ParentLettersState {
|
||||
const factory ParentLettersState({
|
||||
@Default([]) List<ParentLetterSummary> letters,
|
||||
@Default(false) bool hasMore,
|
||||
@Default(0) int unreadCount,
|
||||
@Default(0) int openCount,
|
||||
}) = _ParentLettersState;
|
||||
|
||||
factory ParentLettersState.fromJson(Map<String, dynamic> json) =>
|
||||
_$ParentLettersStateFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'parent_letters_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ParentLettersState {
|
||||
|
||||
List<ParentLetterSummary> get letters; bool get hasMore; int get unreadCount; int get openCount;
|
||||
/// Create a copy of ParentLettersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ParentLettersStateCopyWith<ParentLettersState> get copyWith => _$ParentLettersStateCopyWithImpl<ParentLettersState>(this as ParentLettersState, _$identity);
|
||||
|
||||
/// Serializes this ParentLettersState to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
final _this = this as ParentLettersState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLettersState&&const DeepCollectionEquality().equals(other.letters, _this.letters)&&(identical(other.hasMore, _this.hasMore) || other.hasMore == _this.hasMore)&&(identical(other.unreadCount, _this.unreadCount) || other.unreadCount == _this.unreadCount)&&(identical(other.openCount, _this.openCount) || other.openCount == _this.openCount));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
final _this = this as ParentLettersState;
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.letters),_this.hasMore,_this.unreadCount,_this.openCount);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final _this = this as ParentLettersState;
|
||||
return 'ParentLettersState(letters: ${_this.letters}, hasMore: ${_this.hasMore}, unreadCount: ${_this.unreadCount}, openCount: ${_this.openCount})';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ParentLettersStateCopyWith<$Res> {
|
||||
factory $ParentLettersStateCopyWith(ParentLettersState value, $Res Function(ParentLettersState) _then) = _$ParentLettersStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ParentLetterSummary> letters, bool hasMore, int unreadCount, int openCount
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ParentLettersStateCopyWithImpl<$Res>
|
||||
implements $ParentLettersStateCopyWith<$Res> {
|
||||
_$ParentLettersStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ParentLettersState _self;
|
||||
final $Res Function(ParentLettersState) _then;
|
||||
|
||||
/// Create a copy of ParentLettersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? letters = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) {
|
||||
return _then(ParentLettersState(
|
||||
letters: null == letters ? _self.letters : letters // ignore: cast_nullable_to_non_nullable
|
||||
as List<ParentLetterSummary>,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ParentLettersState].
|
||||
extension ParentLettersStatePatterns on ParentLettersState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ParentLettersState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ParentLettersState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ParentLettersState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ParentLetterSummary> letters, bool hasMore, int unreadCount, int openCount)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState() when $default != null:
|
||||
return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ParentLetterSummary> letters, bool hasMore, int unreadCount, int openCount) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState():
|
||||
return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ParentLetterSummary> letters, bool hasMore, int unreadCount, int openCount)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ParentLettersState() when $default != null:
|
||||
return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ParentLettersState implements ParentLettersState {
|
||||
const _ParentLettersState({ List<ParentLetterSummary> letters = const [], this.hasMore = false, this.unreadCount = 0, this.openCount = 0}): _letters = letters;
|
||||
factory _ParentLettersState.fromJson(Map<String, dynamic> json) => _$ParentLettersStateFromJson(json);
|
||||
|
||||
final List<ParentLetterSummary> _letters;
|
||||
@override@JsonKey() List<ParentLetterSummary> get letters {
|
||||
if (_letters is EqualUnmodifiableListView) return _letters;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_letters);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool hasMore;
|
||||
@override@JsonKey() final int unreadCount;
|
||||
@override@JsonKey() final int openCount;
|
||||
|
||||
/// Create a copy of ParentLettersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ParentLettersStateCopyWith<_ParentLettersState> get copyWith => __$ParentLettersStateCopyWithImpl<_ParentLettersState>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ParentLettersStateToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLettersState&&const DeepCollectionEquality().equals(other.letters, _letters)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)&&(identical(other.openCount, openCount) || other.openCount == openCount));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_letters),hasMore,unreadCount,openCount);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ParentLettersState(letters: $letters, hasMore: $hasMore, unreadCount: $unreadCount, openCount: $openCount)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ParentLettersStateCopyWith<$Res> implements $ParentLettersStateCopyWith<$Res> {
|
||||
factory _$ParentLettersStateCopyWith(_ParentLettersState value, $Res Function(_ParentLettersState) _then) = __$ParentLettersStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ParentLetterSummary> letters, bool hasMore, int unreadCount, int openCount
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ParentLettersStateCopyWithImpl<$Res>
|
||||
implements _$ParentLettersStateCopyWith<$Res> {
|
||||
__$ParentLettersStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ParentLettersState _self;
|
||||
final $Res Function(_ParentLettersState) _then;
|
||||
|
||||
/// Create a copy of ParentLettersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? letters = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) {
|
||||
return _then(_ParentLettersState(
|
||||
letters: null == letters ? _self._letters : letters // ignore: cast_nullable_to_non_nullable
|
||||
as List<ParentLetterSummary>,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,29 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'parent_letters_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ParentLettersState _$ParentLettersStateFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLettersState(
|
||||
letters:
|
||||
(json['letters'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ParentLetterSummary.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
hasMore: json['hasMore'] as bool? ?? false,
|
||||
unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0,
|
||||
openCount: (json['openCount'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLettersStateToJson(_ParentLettersState instance) =>
|
||||
<String, dynamic>{
|
||||
'letters': instance.letters,
|
||||
'hasMore': instance.hasMore,
|
||||
'unreadCount': instance.unreadCount,
|
||||
'openCount': instance.openCount,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
import 'bloc/parent_letters_state.dart';
|
||||
|
||||
ParentLettersState firstPageState(ParentLetterListResponse page) =>
|
||||
ParentLettersState(
|
||||
letters: page.items,
|
||||
hasMore: page.hasMore,
|
||||
unreadCount: page.unreadCount,
|
||||
openCount: page.openCount,
|
||||
);
|
||||
|
||||
ParentLettersState appendOlderPage(
|
||||
ParentLettersState state,
|
||||
ParentLetterListResponse page,
|
||||
) {
|
||||
final known = {for (final letter in state.letters) letter.id};
|
||||
return state.copyWith(
|
||||
letters: [
|
||||
...state.letters,
|
||||
...page.items.where((letter) => !known.contains(letter.id)),
|
||||
],
|
||||
hasMore: page.hasMore,
|
||||
unreadCount: page.unreadCount,
|
||||
openCount: page.openCount,
|
||||
);
|
||||
}
|
||||
|
||||
/// Marks [letterId] as read and lowers the unread counter. Returns [state]
|
||||
/// itself when the letter is unknown or already read.
|
||||
ParentLettersState withLetterRead(ParentLettersState state, String letterId) {
|
||||
final index = state.letters.indexWhere((letter) => letter.id == letterId);
|
||||
if (index < 0 || state.letters[index].read) return state;
|
||||
final letters = [...state.letters];
|
||||
letters[index] = letters[index].copyWith(read: true);
|
||||
return state.copyWith(
|
||||
letters: letters,
|
||||
unreadCount: state.unreadCount > 0 ? state.unreadCount - 1 : 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the letter and moves [ParentLettersState.openCount] along with
|
||||
/// its status, so answering does not need a reload to fix the counter.
|
||||
ParentLettersState withSummary(
|
||||
ParentLettersState state,
|
||||
ParentLetterSummary summary,
|
||||
) {
|
||||
final index = state.letters.indexWhere((letter) => letter.id == summary.id);
|
||||
if (index < 0) return state;
|
||||
int open(ParentLetterSummary letter) =>
|
||||
letter.status == ParentLetterStatus.open ? 1 : 0;
|
||||
final openCount =
|
||||
state.openCount - open(state.letters[index]) + open(summary);
|
||||
final letters = [...state.letters];
|
||||
letters[index] = summary;
|
||||
return state.copyWith(
|
||||
letters: letters,
|
||||
openCount: openCount < 0 ? 0 : openCount,
|
||||
);
|
||||
}
|
||||
|
||||
/// [childId] null means all children.
|
||||
List<ParentLetterSummary> filterLettersByChild(
|
||||
List<ParentLetterSummary> letters,
|
||||
String? childId,
|
||||
) => childId == null
|
||||
? letters
|
||||
: letters.where((letter) => letter.childIds.contains(childId)).toList();
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../../../../../api/demo/data/demo_parent_letters.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letter.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart';
|
||||
import '../../../infrastructure/repository/repository.dart';
|
||||
import '../bloc/parent_letter_state.dart';
|
||||
|
||||
/// Split from [ParentLettersRepository] because the loadable base binds a
|
||||
/// repository to its state type. Writes never reach this class in demo mode
|
||||
/// (the views guard them).
|
||||
class ParentLetterRepository extends Repository<ParentLetterState> {
|
||||
Future<ParentLetterDetail> getLetter(String letterId) {
|
||||
if (DemoMode.active) {
|
||||
return Future.value(DemoParentLetters.detail(letterId));
|
||||
}
|
||||
return GetParentLetter().run(letterId);
|
||||
}
|
||||
|
||||
Future<ParentLetterDetail> submitResponse({
|
||||
required String letterId,
|
||||
required String childId,
|
||||
required List<ParentLetterAnswer> answers,
|
||||
Uint8List? signaturePng,
|
||||
}) => SubmitParentLetterResponse().run(
|
||||
letterId: letterId,
|
||||
childId: childId,
|
||||
answers: answers,
|
||||
signaturePng: signaturePng,
|
||||
);
|
||||
|
||||
Future<ParentLetterThreadMessage> sendThreadMessage({
|
||||
required String letterId,
|
||||
required String body,
|
||||
required String clientMessageId,
|
||||
}) => PostParentLetterThreadMessage().run(
|
||||
letterId: letterId,
|
||||
body: body,
|
||||
clientMessageId: clientMessageId,
|
||||
);
|
||||
|
||||
Future<Uint8List> getAttachment({
|
||||
required String letterId,
|
||||
required String attachmentId,
|
||||
}) => GetParentLetterAttachment().run(
|
||||
letterId: letterId,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import '../../../../../api/demo/data/demo_parent_letters.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letters.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart';
|
||||
import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
||||
import '../../../infrastructure/repository/repository.dart';
|
||||
import '../bloc/parent_letters_state.dart';
|
||||
|
||||
class ParentLettersRepository extends Repository<ParentLettersState> {
|
||||
Future<ParentLetterListResponse> getLetters({String? before}) {
|
||||
if (DemoMode.active) return Future.value(DemoParentLetters.list());
|
||||
return GetParentLetters().run(before: before);
|
||||
}
|
||||
|
||||
Future<void> markRead(String letterId) {
|
||||
if (DemoMode.active) {
|
||||
DemoParentLetters.markRead(letterId);
|
||||
return Future.value();
|
||||
}
|
||||
return MarkParentLetterRead().run(letterId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user