diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 07efebe06a..d55f0e42b5 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; part 'channel_model.g.dart'; @@ -94,7 +95,7 @@ class ChannelModel { /// The date at which the channel was last updated. @JsonKey(includeToJson: false, includeFromJson: false) - DateTime? get lastUpdatedAt => lastMessageAt ?? createdAt; + DateTime get lastUpdatedAt => lastMessageAt ?? createdAt; /// The date of the last channel update @JsonKey(includeToJson: false) @@ -118,18 +119,21 @@ class ChannelModel { /// True if the channel is disabled @JsonKey(includeToJson: false, includeFromJson: false) - bool? get disabled => extraData['disabled'] as bool?; + bool? get disabled => extraData['disabled'].safeCast(); /// True if the channel is hidden @JsonKey(includeToJson: false, includeFromJson: false) - bool? get hidden => extraData['hidden'] as bool?; + bool? get hidden => extraData['hidden'].safeCast(); /// The date of the last time channel got truncated @JsonKey(includeToJson: false, includeFromJson: false) DateTime? get truncatedAt { - final truncatedAt = extraData['truncated_at'] as String?; - if (truncatedAt == null) return null; - return DateTime.parse(truncatedAt); + final truncatedAt = extraData['truncated_at'].safeCast(); + if (truncatedAt != null && truncatedAt.isNotEmpty) { + return DateTime.parse(truncatedAt); + } + + return null; } /// Map of custom channel extraData @@ -139,6 +143,14 @@ class ChannelModel { @JsonKey(includeToJson: false) final String? team; + /// Shortcut for channel name + String? get name { + final name = extraData['name'].safeCast(); + if (name != null && name.isNotEmpty) return name; + + return null; + } + /// Known top level fields. /// Useful for [Serializer] methods. static const topLevelFields = [ @@ -159,9 +171,6 @@ class ChannelModel { 'cooldown', ]; - /// Shortcut for channel name - String? get name => extraData['name'] as String?; - /// Serialize to json Map toJson() => Serializer.moveFromExtraDataToRoot( _$ChannelModelToJson(this), diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 3e5b9b03c3..4aebd27c42 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/comparable_field.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; part 'user.g.dart'; @@ -83,10 +84,9 @@ class User extends Equatable implements ComparableFieldProvider { /// {@macro name} @JsonKey(includeToJson: false, includeFromJson: false) String get name { - if (extraData.containsKey('name') && extraData['name'] != null) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } + final name = extraData['name'].safeCast(); + if (name != null && name.isNotEmpty) return name; + return id; } @@ -94,7 +94,12 @@ class User extends Equatable implements ComparableFieldProvider { /// /// {@macro image} @JsonKey(includeToJson: false, includeFromJson: false) - String? get image => extraData['image'] as String?; + String? get image { + final image = extraData['image'].safeCast(); + if (image != null && image.isNotEmpty) return image; + + return null; + } /// User role. @JsonKey(includeToJson: false) diff --git a/packages/stream_chat/lib/src/core/util/extension.dart b/packages/stream_chat/lib/src/core/util/extension.dart index 764532aa2f..752889bfbb 100644 --- a/packages/stream_chat/lib/src/core/util/extension.dart +++ b/packages/stream_chat/lib/src/core/util/extension.dart @@ -92,3 +92,14 @@ extension IterableMergeExtension on Iterable { return itemMap.values; } } + +/// Extension on [Object] providing safe casting functionality. +extension SafeCastExtension on Object? { + /// Safely casts the object to a specific type. + /// + /// Returns null if the object is null or cannot be cast to the type. + T? safeCast() { + if (this is T) return this as T; + return null; + } +} diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index 04925fdd81..50587cd573 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:test/test.dart'; @@ -188,4 +190,131 @@ void main() { expect(newChannel.extraData['truncated_at'], dateThree.toIso8601String()); expect(newChannel.truncatedAt, dateThree); }); + + test('.name should fetch from extraData if available', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'name': 'Test Channel'}, + ); + + expect(channel.name, 'Test Channel'); + }); + + test('.name should return null if not available in extraData', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {}, + ); + + expect(channel.name, isNull); + }); + + test('.name should return null if extraData value is not String', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'name': true}, + ); + + expect(channel.name, isNull); + }); + + test('.name should return null if extraData value is empty', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'name': ''}, + ); + + expect(channel.name, isNull); + }); + + test('.disabled should fetch from extraData if available', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'disabled': true}, + ); + + expect(channel.disabled, true); + }); + + test('.disabled should return null if not available in extraData', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {}, + ); + + expect(channel.disabled, isNull); + }); + + test('.disabled should return null if extraData value is not bool', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'disabled': 'true'}, + ); + + expect(channel.disabled, isNull); + }); + + test('.hidden should fetch from extraData if available', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'hidden': true}, + ); + + expect(channel.hidden, true); + }); + + test('.hidden should return null if not available in extraData', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {}, + ); + + expect(channel.hidden, isNull); + }); + + test('.hidden should return null if extraData value is not bool', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'hidden': 'false'}, + ); + + expect(channel.hidden, isNull); + }); + + test('.truncatedAt should fetch from extraData if available', () { + final testDate = DateTime.now(); + final channel = ChannelModel( + cid: 'test:channel', + extraData: {'truncated_at': testDate.toIso8601String()}, + ); + + expect(channel.truncatedAt, testDate); + }); + + test('.truncatedAt should return null if not available in extraData', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {}, + ); + + expect(channel.truncatedAt, isNull); + }); + + test('.truncatedAt should return null if extraData value is not String', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'truncated_at': true}, + ); + + expect(channel.truncatedAt, isNull); + }); + + test('.truncatedAt should return null if extraData value is empty', () { + final channel = ChannelModel( + cid: 'test:channel', + extraData: const {'truncated_at': ''}, + ); + + expect(channel.truncatedAt, isNull); + }); } diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index 1b7b6f43ac..b82fb35481 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -366,6 +366,78 @@ void main() { expect(field!.value, equals('without-name')); // Fallback to user id }); }); + + test('.name should fetch from extraData if available', () { + final user = User( + id: 'test-user', + extraData: const {'name': 'Test User'}, + ); + + expect(user.name, 'Test User'); + }); + + test('.name should return id if extraData value is empty', () { + final user = User( + id: 'test-user', + extraData: const {'name': ''}, + ); + + expect(user.name, 'test-user'); + }); + + test('.name should return id if not available in extraData', () { + final user = User( + id: 'test-user', + extraData: const {}, + ); + + expect(user.name, 'test-user'); + }); + + test('.name should return id if extraData value is not String', () { + final user = User( + id: 'test-user', + extraData: const {'name': true}, + ); + + expect(user.name, 'test-user'); + }); + + test('.image should fetch from extraData if available', () { + final user = User( + id: 'test-user', + extraData: const {'image': 'https://example.com/image.png'}, + ); + + expect(user.image, 'https://example.com/image.png'); + }); + + test('.image should return null if not available in extraData', () { + final user = User( + id: 'test-user', + extraData: const {}, + ); + + expect(user.image, isNull); + }); + + test('.image should return null if extraData value is not String', () { + final user = User( + id: 'test-user', + extraData: const {'image': true}, + ); + + expect(user.image, isNull); + }); + + test('.image should return null if extraData value is empty', () { + final user = User( + id: 'test-user', + extraData: const {'image': ''}, + ); + + expect(user.image, isNull); + }); }); }