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
203 lines
5.3 KiB
Dart
203 lines
5.3 KiB
Dart
import 'package:json_annotation/json_annotation.dart';
|
|
|
|
import '../../../../extensions/date_time.dart';
|
|
import '../../../api_response.dart';
|
|
import '../../../json_lenient.dart';
|
|
import '../room/get_room_response.dart';
|
|
|
|
part 'get_chat_response.g.dart';
|
|
|
|
@JsonSerializable(explicitToJson: true)
|
|
class GetChatResponse extends ApiResponse {
|
|
@JsonKey(fromJson: _messagesFromJson)
|
|
Set<GetChatResponseObject> data;
|
|
|
|
GetChatResponse(this.data);
|
|
|
|
factory GetChatResponse.fromJson(Map<String, dynamic> json) =>
|
|
_$GetChatResponseFromJson(json);
|
|
Map<String, dynamic> toJson() => _$GetChatResponseToJson(this);
|
|
|
|
List<GetChatResponseObject> sortByTimestamp() {
|
|
var sorted = data.toList();
|
|
sorted.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
return sorted;
|
|
}
|
|
}
|
|
|
|
Set<GetChatResponseObject> _messagesFromJson(Object? raw) =>
|
|
decodeSetLenient(raw, GetChatResponseObject.fromJson, what: 'message');
|
|
|
|
@JsonSerializable(explicitToJson: true)
|
|
class GetChatResponseObject {
|
|
int id;
|
|
String token;
|
|
@JsonKey(unknownEnumValue: GetRoomResponseObjectMessageActorType.unknown)
|
|
GetRoomResponseObjectMessageActorType actorType;
|
|
String actorId;
|
|
String actorDisplayName;
|
|
int timestamp;
|
|
String systemMessage;
|
|
@JsonKey(unknownEnumValue: GetRoomResponseObjectMessageType.unknown)
|
|
GetRoomResponseObjectMessageType messageType;
|
|
bool isReplyable;
|
|
String referenceId;
|
|
String message;
|
|
Map<String, int>? reactions;
|
|
List<String>? reactionsSelf;
|
|
@JsonKey(fromJson: _fromJson)
|
|
Map<String, RichObjectString>? messageParameters;
|
|
|
|
/// The quoted message. When the original is no longer available (expired,
|
|
/// purged) the server only sends `{id, deleted: true}`; that becomes a
|
|
/// deleted-comment placeholder so the quote still renders.
|
|
@JsonKey(fromJson: _parentFromJson)
|
|
GetChatResponseObject? parent;
|
|
|
|
/// Whether the server flagged this message to be rendered as Markdown
|
|
/// (capability `markdown-messages`). Absent on older servers/messages, in
|
|
/// which case it stays `false` and the body renders as plain text.
|
|
bool markdown;
|
|
|
|
GetChatResponseObject(
|
|
this.id,
|
|
this.token,
|
|
this.actorType,
|
|
this.actorId,
|
|
this.actorDisplayName,
|
|
this.timestamp,
|
|
this.systemMessage,
|
|
this.messageType,
|
|
this.isReplyable,
|
|
this.referenceId,
|
|
this.message,
|
|
this.messageParameters,
|
|
this.reactions,
|
|
this.reactionsSelf,
|
|
this.parent, [
|
|
this.markdown = false,
|
|
]);
|
|
|
|
factory GetChatResponseObject.fromJson(Map<String, dynamic> json) =>
|
|
_$GetChatResponseObjectFromJson(json);
|
|
Map<String, dynamic> toJson() => _$GetChatResponseObjectToJson(this);
|
|
|
|
/// [timestamp] is in seconds.
|
|
DateTime get sentAt => DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
|
|
static GetChatResponseObject getDateDummy(int timestamp) => getTextDummy(
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
timestamp * 1000,
|
|
).formatDateRelativeShort(),
|
|
);
|
|
|
|
static GetChatResponseObject deletedPlaceholder(int id) =>
|
|
GetChatResponseObject(
|
|
id,
|
|
'',
|
|
GetRoomResponseObjectMessageActorType.user,
|
|
'',
|
|
'',
|
|
0,
|
|
'',
|
|
GetRoomResponseObjectMessageType.deletedComment,
|
|
false,
|
|
'',
|
|
'Nachricht wurde gelöscht',
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
);
|
|
|
|
static GetChatResponseObject getTextDummy(String text) =>
|
|
GetChatResponseObject(
|
|
0,
|
|
'',
|
|
GetRoomResponseObjectMessageActorType.user,
|
|
'',
|
|
'',
|
|
0,
|
|
'',
|
|
GetRoomResponseObjectMessageType.system,
|
|
false,
|
|
'',
|
|
text,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
);
|
|
}
|
|
|
|
GetChatResponseObject? _parentFromJson(Object? json) {
|
|
if (json is! Map<String, dynamic>) return null;
|
|
if (json['deleted'] == true) {
|
|
return GetChatResponseObject.deletedPlaceholder(
|
|
(json['id'] as num?)?.toInt() ?? 0,
|
|
);
|
|
}
|
|
return GetChatResponseObject.fromJson(json);
|
|
}
|
|
|
|
Map<String, RichObjectString>? _fromJson(dynamic json) {
|
|
if (json is Map<String, dynamic>) {
|
|
final data = <String, RichObjectString>{};
|
|
for (final entry in json.entries) {
|
|
final value = entry.value;
|
|
if (value is Map<String, dynamic>) {
|
|
data[entry.key] = RichObjectString.fromJson(value);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@JsonSerializable(explicitToJson: true)
|
|
class RichObjectString {
|
|
@JsonKey(unknownEnumValue: RichObjectStringObjectType.unknown)
|
|
RichObjectStringObjectType type;
|
|
String id;
|
|
String name;
|
|
|
|
String? path;
|
|
String? link;
|
|
|
|
RichObjectString(this.type, this.id, this.name, this.path, this.link);
|
|
|
|
factory RichObjectString.fromJson(Map<String, dynamic> json) =>
|
|
_$RichObjectStringFromJson(json);
|
|
Map<String, dynamic> toJson() => _$RichObjectStringToJson(this);
|
|
}
|
|
|
|
enum RichObjectStringObjectType {
|
|
@JsonValue('user')
|
|
user,
|
|
@JsonValue('group')
|
|
group,
|
|
@JsonValue('file')
|
|
file,
|
|
@JsonValue('guest')
|
|
guest,
|
|
@JsonValue('highlight')
|
|
highlight,
|
|
@JsonValue('talk-poll')
|
|
talkPoll,
|
|
@JsonValue('geo-location')
|
|
geoLocation,
|
|
@JsonValue('call')
|
|
call,
|
|
@JsonValue('user-group')
|
|
userGroup,
|
|
@JsonValue('circle')
|
|
circle,
|
|
@JsonValue('email')
|
|
email,
|
|
|
|
/// Anything else (deck cards, contacts, calendar events, ...); the
|
|
/// placeholder still resolves to the object's name.
|
|
unknown,
|
|
}
|