hardened API decoding against server fields the models did not expect

The Talk room list crashed for a new account because the server omits
lastMessage for rooms without a visible message and the model required it.
Beyond that single field, a review of the Talk models against the OpenAPI
spec of the installed version (23.0.8) found the same crash class in several
places: closed enums over open server values (inCall bitmask, rich-object
types, actor and message types), a deleted parent sent as {id, deleted},
and the autocomplete status arriving as an object.

- json_serializable `checked: true`: decode errors now name class and field
- lists (rooms, messages, participants, autocomplete) decode per entry;
  a malformed entry is skipped and reported via ClientErrorReporter
- unknownEnumValue fallbacks for all response enums of Talk and breakers
- lastMessage nullable; inCall as bitmask; deleted parent placeholder;
  autocomplete status object/string; fixed shareWithDisplayNameUnique key
- GetRoomParams no longer sends null query parameters

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrepDTL4qDe35TuS29sLW3
This commit is contained in:
2026-09-29 16:35:41 +02:00
co-authored by Claude Fable 5.1
parent a79b376bb3
commit 279b973e89
92 changed files with 2150 additions and 963 deletions
@@ -1,12 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
import '../../../json_lenient.dart';
import '../chat/get_chat_response.dart';
part 'get_room_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetRoomResponse extends ApiResponse {
@JsonKey(fromJson: _roomsFromJson)
Set<GetRoomResponseObject> data;
GetRoomResponse(this.data);
@@ -33,10 +35,14 @@ class GetRoomResponse extends ApiResponse {
}
}
Set<GetRoomResponseObject> _roomsFromJson(Object? raw) =>
decodeSetLenient(raw, GetRoomResponseObject.fromJson, what: 'room');
@JsonSerializable(explicitToJson: true)
class GetRoomResponseObject {
int id;
String token;
@JsonKey(unknownEnumValue: GetRoomResponseObjectConversationType.unknown)
GetRoomResponseObjectConversationType type;
String name;
String displayName;
@@ -55,13 +61,20 @@ class GetRoomResponseObject {
bool canLeaveConversation;
int lastActivity;
bool isFavorite;
@JsonKey(
unknownEnumValue:
GetRoomResponseObjectParticipantNotificationLevel.defaultLevel,
)
GetRoomResponseObjectParticipantNotificationLevel notificationLevel;
int unreadMessages;
bool unreadMention;
bool unreadMentionDirect;
int lastReadMessage;
int lastCommonReadMessage;
GetChatResponseObject lastMessage;
/// Omitted by the server for rooms without a visible message (empty room,
/// last message expired or hidden from this user, federated conversation).
GetChatResponseObject? lastMessage;
String? status;
String? statusIcon;
String? statusMessage;
@@ -117,6 +130,9 @@ enum GetRoomResponseObjectConversationType {
deleted,
@JsonValue(6)
noteToSelf,
/// Fallback for types this app does not know yet; treated like a group.
unknown,
}
enum GetRoomResponseObjectParticipantNotificationLevel {
@@ -147,6 +163,17 @@ enum GetRoomResponseObjectMessageActorType {
bot,
@JsonValue('bridged')
bridge,
@JsonValue('groups')
group,
@JsonValue('emails')
email,
@JsonValue('circles')
circle,
@JsonValue('federated_users')
federatedUser,
@JsonValue('phones')
phone,
unknown,
}
enum GetRoomResponseObjectMessageType {
@@ -160,4 +187,8 @@ enum GetRoomResponseObjectMessageType {
system,
@JsonValue('command')
command,
/// Fallback for types this app does not know yet (e.g. call recordings);
/// rendered like a plain comment.
unknown,
}