Merge remote-tracking branch 'origin/develop' into refactor/message-status

This commit is contained in:
xsahil03x
2023-06-20 14:32:30 +05:30
34 changed files with 830 additions and 303 deletions
@@ -46,7 +46,7 @@ jobs:
run: | run: |
melos run analyze melos run analyze
- name: "Pub Check" - name: "Pub Check"
if: github.ref == 'refs/heads/master' if: github.base_ref == 'master'
run: | run: |
melos run lint:pub melos run lint:pub
+1 -1
View File
@@ -2,7 +2,7 @@
![](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/images/sdk_hero_v4.png) ![](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/images/sdk_hero_v4.png)
![CI](https://github.com/GetStream/stream-chat-flutter/workflows/stream_flutter_workflow/badge.svg?branch=master) [![CI](https://github.com/GetStream/stream-chat-flutter/actions/workflows/stream_flutter_workflow.yml/badge.svg?branch=master)](https://github.com/GetStream/stream-chat-flutter/actions/workflows/stream_flutter_workflow.yml)
[![Melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) [![Melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos)
**Quick Links** **Quick Links**
+3 -1
View File
@@ -1,9 +1,11 @@
## Upcoming ## 6.4.0
🐞 Fixed 🐞 Fixed
- [[#1293]](https://github.com/GetStream/stream-chat-flutter/issues/1293) Fixed wrong message order when sending - [[#1293]](https://github.com/GetStream/stream-chat-flutter/issues/1293) Fixed wrong message order when sending
messages quickly. messages quickly.
- [[#1612]](https://github.com/GetStream/stream-chat-flutter/issues/1612) Fixed `Channel.isMutedStream` does not emit
when channel mute expires.
## 6.3.0 ## 6.3.0
@@ -1510,15 +1510,32 @@ class Channel {
); );
} }
// Timer to keep track of mute expiration. This is used to update the channel
// state when the mute expires.
Timer? _muteExpirationTimer;
/// Mutes the channel. /// Mutes the channel.
Future<EmptyResponse> mute({Duration? expiration}) { Future<EmptyResponse> mute({Duration? expiration}) {
_checkInitialized(); _checkInitialized();
// If there is a expiration set, we will set a timer to automatically unmute
// the channel when the mute expires.
if (expiration != null) {
_muteExpirationTimer?.cancel();
_muteExpirationTimer = Timer(expiration, unmute);
}
return _client.muteChannel(cid!, expiration: expiration); return _client.muteChannel(cid!, expiration: expiration);
} }
/// Unmute the channel. /// Unmute the channel.
Future<EmptyResponse> unmute() { Future<EmptyResponse> unmute() {
_checkInitialized(); _checkInitialized();
// Cancel the mute expiration timer if it is set.
_muteExpirationTimer?.cancel();
_muteExpirationTimer = null;
return _client.unmuteChannel(cid!); return _client.unmuteChannel(cid!);
} }
@@ -1648,6 +1665,7 @@ class Channel {
void dispose() { void dispose() {
client.state.removeChannel('$cid'); client.state.removeChannel('$cid');
state?.dispose(); state?.dispose();
_muteExpirationTimer?.cancel();
_keyStrokeHandler.cancel(); _keyStrokeHandler.cancel();
} }
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '6.3.0'; const PACKAGE_VERSION = '6.4.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. description: The official Dart client for Stream Chat, a service for building chat applications.
version: 6.3.0 version: 6.4.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -2481,6 +2481,31 @@ void main() {
)).called(1); )).called(1);
}); });
test('`.mute with expiration`', () async {
const expiration = Duration(seconds: 3);
when(() => client.muteChannel(
channelCid,
expiration: expiration,
)).thenAnswer((_) async => EmptyResponse());
when(() => client.unmuteChannel(channelCid))
.thenAnswer((_) async => EmptyResponse());
final res = await channel.mute(expiration: expiration);
expect(res, isNotNull);
verify(() => client.muteChannel(
channelCid,
expiration: expiration,
)).called(1);
// wait for expiration
await Future.delayed(expiration);
verify(() => client.unmuteChannel(channelCid)).called(1);
});
test('`.unmute`', () async { test('`.unmute`', () async {
when( when(
() => client.unmuteChannel(channelCid), () => client.unmuteChannel(channelCid),
+44 -1
View File
@@ -1,4 +1,4 @@
## Upcoming ## 6.4.0
🐞 Fixed 🐞 Fixed
@@ -52,6 +52,49 @@
) )
``` ```
- Added support for `StreamChannelAvatar.ownSpaceAvatarBuilder`, `StreamChannelAvatar.oneToOneAvatarBuilder` and
`StreamChannelAvatar.groupAvatarBuilder` to override the default avatar
widget.[#1614](https://github.com/GetStream/stream-chat-flutter/issues/1614)
```dart
StreamChannelAvatar(
...,
ownSpaceAvatarBuilder: (context, channel) {
return Container(
color: Colors.red,
child: Text('Own Space Avatar'),
);
},
oneToOneAvatarBuilder: (context, channel) {
return Container(
color: Colors.red,
child: Text('One to One Avatar'),
);
},
groupAvatarBuilder: (context, channel) {
return Container(
color: Colors.red,
child: Text('Group Avatar'),
);
},
)
```
- Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content insertion configuration.
[#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613)
```dart
StreamMessageInput(
...,
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (content) {
// Do something with the content.
controller.addAttachment(...);
},
),
)
```
## 6.3.0 ## 6.3.0
🐞 Fixed 🐞 Fixed
@@ -1,6 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// WidgetBuilder for [StreamGroupAvatar].
typedef StreamGroupAvatarBuilder = Widget Function(
BuildContext context,
List<Member> members,
// ignore: avoid_positional_boolean_parameters
bool isSelected,
);
/// {@template streamGroupAvatar} /// {@template streamGroupAvatar}
/// Widget for constructing a group of images /// Widget for constructing a group of images
/// {@endtemplate} /// {@endtemplate}
@@ -2,6 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// WidgetBuilder for [StreamUserAvatar].
typedef StreamUserAvatarBuilder = Widget Function(
BuildContext context,
User user,
// ignore: avoid_positional_boolean_parameters
bool isSelected,
);
/// {@template streamUserAvatar} /// {@template streamUserAvatar}
/// Displays a user's avatar. /// Displays a user's avatar.
/// {@endtemplate} /// {@endtemplate}
@@ -54,6 +54,9 @@ class StreamChannelAvatar extends StatelessWidget {
this.selected = false, this.selected = false,
this.selectionColor, this.selectionColor,
this.selectionThickness = 4, this.selectionThickness = 4,
this.ownSpaceAvatarBuilder,
this.oneToOneAvatarBuilder,
this.groupAvatarBuilder,
}) : assert( }) : assert(
channel.state != null, channel.state != null,
'Channel ${channel.id} is not initialized', 'Channel ${channel.id} is not initialized',
@@ -80,6 +83,21 @@ class StreamChannelAvatar extends StatelessWidget {
/// Thickness of selection image /// Thickness of selection image
final double selectionThickness; final double selectionThickness;
/// Builder to create avatar for own space channel.
///
/// Defaults to [StreamUserAvatar].
final StreamUserAvatarBuilder? ownSpaceAvatarBuilder;
/// Builder to create avatar for one to one channel.
///
/// Defaults to [StreamUserAvatar].
final StreamUserAvatarBuilder? oneToOneAvatarBuilder;
/// Builder to create avatar for group channel.
///
/// Defaults to [StreamGroupAvatar].
final StreamGroupAvatarBuilder? groupAvatarBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = channel.client.state; final client = channel.client.state;
@@ -146,15 +164,22 @@ class StreamChannelAvatar extends StatelessWidget {
return BetterStreamBuilder<User>( return BetterStreamBuilder<User>(
stream: client.currentUserStream.map((it) => it!), stream: client.currentUserStream.map((it) => it!),
initialData: currentUser, initialData: currentUser,
builder: (context, user) => StreamUserAvatar( builder: (context, user) {
borderRadius: borderRadius ?? previewTheme?.borderRadius, final ownSpaceBuilder = ownSpaceAvatarBuilder;
user: user, if (ownSpaceBuilder != null) {
constraints: constraints ?? previewTheme?.constraints, return ownSpaceBuilder(context, user, selected);
onTap: onTap != null ? (_) => onTap!() : null, }
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary, return StreamUserAvatar(
selectionThickness: selectionThickness, borderRadius: borderRadius ?? previewTheme?.borderRadius,
), user: user,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
);
},
); );
} }
@@ -169,18 +194,30 @@ class StreamChannelAvatar extends StatelessWidget {
), ),
), ),
initialData: member, initialData: member,
builder: (context, member) => StreamUserAvatar( builder: (context, member) {
borderRadius: borderRadius ?? previewTheme?.borderRadius, final oneToOneBuilder = oneToOneAvatarBuilder;
user: member.user!, if (oneToOneBuilder != null) {
constraints: constraints ?? previewTheme?.constraints, return oneToOneBuilder(context, member.user!, selected);
onTap: onTap != null ? (_) => onTap!() : null, }
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary, return StreamUserAvatar(
selectionThickness: selectionThickness, borderRadius: borderRadius ?? previewTheme?.borderRadius,
), user: member.user!,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
);
},
); );
} }
final groupBuilder = groupAvatarBuilder;
if (groupBuilder != null) {
return groupBuilder(context, otherMembers, selected);
}
// Group conversation // Group conversation
return StreamGroupAvatar( return StreamGroupAvatar(
channel: channel, channel: channel,
@@ -145,6 +145,7 @@ class StreamMessageInput extends StatefulWidget {
_defaultClearQuotedMessageKeyPredicate, _defaultClearQuotedMessageKeyPredicate,
this.ogPreviewFilter = _defaultOgPreviewFilter, this.ogPreviewFilter = _defaultOgPreviewFilter,
this.hintGetter = _defaultHintGetter, this.hintGetter = _defaultHintGetter,
this.contentInsertionConfiguration,
}); });
/// The predicate used to send a message on desktop/web /// The predicate used to send a message on desktop/web
@@ -306,6 +307,9 @@ class StreamMessageInput extends StatefulWidget {
/// Returns the hint text for the message input. /// Returns the hint text for the message input.
final HintGetter hintGetter; final HintGetter hintGetter;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
static String? _defaultHintGetter( static String? _defaultHintGetter(
BuildContext context, BuildContext context,
HintType type, HintType type,
@@ -871,6 +875,8 @@ class StreamMessageInputState extends State<StreamMessageInput>
decoration: _getInputDecoration(context), decoration: _getInputDecoration(context),
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
autocorrect: widget.autoCorrect, autocorrect: widget.autoCorrect,
contentInsertionConfiguration:
widget.contentInsertionConfiguration,
), ),
), ),
), ),
@@ -120,6 +120,7 @@ class StreamMessageTextField extends StatefulWidget {
this.restorationId, this.restorationId,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.contentInsertionConfiguration,
}) : assert(obscuringCharacter.length == 1, ''), }) : assert(obscuringCharacter.length == 1, ''),
smartDashesType = smartDashesType ?? smartDashesType = smartDashesType ??
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
@@ -526,6 +527,9 @@ class StreamMessageTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning; final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
@override @override
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); _StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
@@ -622,6 +626,9 @@ class StreamMessageTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>( properties.add(DiagnosticsProperty<bool>(
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
defaultValue: true)); defaultValue: true));
properties.add(DiagnosticsProperty<ContentInsertionConfiguration>(
'contentInsertionConfiguration', contentInsertionConfiguration,
defaultValue: null));
} }
} }
@@ -727,6 +734,7 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
restorationId: widget.restorationId, restorationId: widget.restorationId,
scribbleEnabled: widget.scribbleEnabled, scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
contentInsertionConfiguration: widget.contentInsertionConfiguration,
); );
@override @override
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 6.3.0 version: 6.4.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -38,7 +38,7 @@ dependencies:
rxdart: ^0.27.0 rxdart: ^0.27.0
share_plus: ^6.3.0 share_plus: ^6.3.0
shimmer: ^3.0.0 shimmer: ^3.0.0
stream_chat_flutter_core: ^6.3.0 stream_chat_flutter_core: ^6.4.0
synchronized: ^3.0.0 synchronized: ^3.0.0
thumblr: ^0.0.4 thumblr: ^0.0.4
url_launcher: ^6.1.0 url_launcher: ^6.1.0
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 100.0; const screenHeight = 100.0;
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0; const screenHeight = 400.0;
@@ -2,12 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0; const screenHeight = 400.0;
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0; const screenHeight = 400.0;
@@ -1,3 +1,7 @@
## 6.4.0
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
## 6.3.0 ## 6.3.0
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 6.3.0 version: 6.4.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -17,7 +17,7 @@ dependencies:
freezed_annotation: ^2.0.3 freezed_annotation: ^2.0.3
meta: ^1.8.0 meta: ^1.8.0
rxdart: ^0.27.0 rxdart: ^0.27.0
stream_chat: ^6.3.0 stream_chat: ^6.4.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
@@ -1,3 +1,7 @@
## 5.4.0
* Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
## 5.3.0 ## 5.3.0
* Updated `stream_chat_flutter` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat_flutter/changelog). * Updated `stream_chat_flutter` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
@@ -1,6 +1,6 @@
name: stream_chat_localizations name: stream_chat_localizations
description: The Official localizations for Stream Chat Flutter, a service for building chat applications description: The Official localizations for Stream Chat Flutter, a service for building chat applications
version: 5.3.0 version: 5.4.0
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -14,7 +14,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
stream_chat_flutter: ^6.3.0 stream_chat_flutter: ^6.4.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^5.7.2 dart_code_metrics: ^5.7.2
@@ -1,3 +1,7 @@
## 6.4.0
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
## 6.3.0 ## 6.3.0
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -5,7 +5,6 @@ import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart';
import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'message_dao.g.dart'; part 'message_dao.g.dart';
@@ -49,7 +49,7 @@ class DriftChatDatabase extends _$DriftChatDatabase {
// you should bump this number whenever you change or add a table definition. // you should bump this number whenever you change or add a table definition.
@override @override
int get schemaVersion => 11; int get schemaVersion => 12;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
File diff suppressed because it is too large Load Diff
@@ -53,14 +53,52 @@ class Messages extends Table {
/// A used command name. /// A used command name.
TextColumn get command => text().nullable()(); TextColumn get command => text().nullable()();
/// The DateTime when the message was created. /// The DateTime on which the message was created.
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); ///
/// Returns the latest between [localCreatedAt] and [remoteCreatedAt].
/// If both are null, returns [currentDateAndTime].
Expression<DateTime> get createdAt {
return coalesce<DateTime>(
[localCreatedAt, remoteCreatedAt, currentDateAndTime],
);
}
/// The DateTime when the message was updated last time. /// The DateTime on which the message was created on the client.
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get localCreatedAt => dateTime().nullable()();
/// The DateTime when the message was deleted. /// The DateTime on which the message was created on the server.
DateTimeColumn get deletedAt => dateTime().nullable()(); DateTimeColumn get remoteCreatedAt => dateTime().nullable()();
/// The DateTime on which the message was updated last time.
///
/// Returns the latest between [localUpdatedAt] and [remoteUpdatedAt].
/// If both are null, returns [createdAt].
Expression<DateTime> get updatedAt {
return coalesce<DateTime>(
[localUpdatedAt, remoteUpdatedAt, createdAt],
);
}
/// The DateTime on which the message was updated on the client.
DateTimeColumn get localUpdatedAt => dateTime().nullable()();
/// The DateTime on which the message was updated on the server.
DateTimeColumn get remoteUpdatedAt => dateTime().nullable()();
/// The DateTime on which the message was deleted.
///
/// Returns the latest between [localDeletedAt] and [remoteDeletedAt].
Expression<DateTime> get deletedAt {
return coalesce<DateTime>(
[localDeletedAt, remoteDeletedAt],
);
}
/// The DateTime on which the message was deleted on the client.
DateTimeColumn get localDeletedAt => dateTime().nullable()();
/// The DateTime on which the message was deleted on the server.
DateTimeColumn get remoteDeletedAt => dateTime().nullable()();
/// Id of the User who sent the message /// Id of the User who sent the message
TextColumn get userId => text().nullable()(); TextColumn get userId => text().nullable()();
@@ -21,9 +21,13 @@ extension MessageEntityX on MessageEntity {
final json = jsonDecode(it); final json = jsonDecode(it);
return Attachment.fromData(json); return Attachment.fromData(json);
}).toList(), }).toList(),
createdAt: createdAt,
extraData: extraData ?? <String, Object>{}, extraData: extraData ?? <String, Object>{},
updatedAt: updatedAt, createdAt: remoteCreatedAt,
localCreatedAt: localCreatedAt,
updatedAt: remoteUpdatedAt,
localUpdatedAt: localUpdatedAt,
deletedAt: remoteDeletedAt,
localDeletedAt: localDeletedAt,
id: id, id: id,
type: type, type: type,
status: status, status: status,
@@ -37,7 +41,6 @@ extension MessageEntityX on MessageEntity {
showInChannel: showInChannel, showInChannel: showInChannel,
text: messageText, text: messageText,
user: user, user: user,
deletedAt: deletedAt,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
pinExpires: pinExpires, pinExpires: pinExpires,
@@ -59,7 +62,8 @@ extension MessageX on Message {
parentId: parentId, parentId: parentId,
quotedMessageId: quotedMessageId, quotedMessageId: quotedMessageId,
command: command, command: command,
createdAt: createdAt, remoteCreatedAt: remoteCreatedAt,
localCreatedAt: localCreatedAt,
shadowed: shadowed, shadowed: shadowed,
showInChannel: showInChannel, showInChannel: showInChannel,
replyCount: replyCount, replyCount: replyCount,
@@ -67,10 +71,12 @@ extension MessageX on Message {
reactionCounts: reactionCounts, reactionCounts: reactionCounts,
mentionedUsers: mentionedUsers.map(jsonEncode).toList(), mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
status: status, status: status,
updatedAt: updatedAt, remoteUpdatedAt: remoteUpdatedAt,
localUpdatedAt: localUpdatedAt,
extraData: extraData, extraData: extraData,
userId: user?.id, userId: user?.id,
deletedAt: deletedAt, remoteDeletedAt: remoteDeletedAt,
localDeletedAt: localDeletedAt,
messageText: text, messageText: text,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
@@ -21,9 +21,13 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
final json = jsonDecode(it); final json = jsonDecode(it);
return Attachment.fromData(json); return Attachment.fromData(json);
}).toList(), }).toList(),
createdAt: createdAt,
extraData: extraData ?? <String, Object>{}, extraData: extraData ?? <String, Object>{},
updatedAt: updatedAt, createdAt: remoteCreatedAt,
localCreatedAt: localCreatedAt,
updatedAt: remoteUpdatedAt,
localUpdatedAt: localUpdatedAt,
deletedAt: remoteDeletedAt,
localDeletedAt: localDeletedAt,
id: id, id: id,
type: type, type: type,
status: status, status: status,
@@ -37,7 +41,6 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
showInChannel: showInChannel, showInChannel: showInChannel,
text: messageText, text: messageText,
user: user, user: user,
deletedAt: deletedAt,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
pinExpires: pinExpires, pinExpires: pinExpires,
@@ -60,7 +63,8 @@ extension PMessageX on Message {
parentId: parentId, parentId: parentId,
quotedMessageId: quotedMessageId, quotedMessageId: quotedMessageId,
command: command, command: command,
createdAt: createdAt, remoteCreatedAt: remoteCreatedAt,
localCreatedAt: localCreatedAt,
shadowed: shadowed, shadowed: shadowed,
showInChannel: showInChannel, showInChannel: showInChannel,
replyCount: replyCount, replyCount: replyCount,
@@ -68,10 +72,12 @@ extension PMessageX on Message {
reactionCounts: reactionCounts, reactionCounts: reactionCounts,
mentionedUsers: mentionedUsers.map(jsonEncode).toList(), mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
status: status, status: status,
updatedAt: updatedAt, remoteUpdatedAt: remoteUpdatedAt,
localUpdatedAt: localUpdatedAt,
extraData: extraData, extraData: extraData,
userId: user?.id, userId: user?.id,
deletedAt: deletedAt, remoteDeletedAt: remoteDeletedAt,
localDeletedAt: localDeletedAt,
messageText: text, messageText: text,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
@@ -1,7 +1,7 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
version: 6.3.0 version: 6.4.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -18,7 +18,7 @@ dependencies:
path: ^1.8.2 path: ^1.8.2
path_provider: ^2.0.1 path_provider: ^2.0.1
sqlite3_flutter_libs: ^0.5.0 sqlite3_flutter_libs: ^0.5.0
stream_chat: ^6.3.0 stream_chat: ^6.4.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
@@ -38,7 +38,8 @@ void main() {
parentId: 'testParentId', parentId: 'testParentId',
quotedMessageId: quotedMessage.id, quotedMessageId: quotedMessage.id,
command: 'testCommand', command: 'testCommand',
createdAt: DateTime.now(), localCreatedAt: DateTime.now(),
remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)),
shadowed: math.Random().nextBool(), shadowed: math.Random().nextBool(),
showInChannel: math.Random().nextBool(), showInChannel: math.Random().nextBool(),
replyCount: 33, replyCount: 33,
@@ -52,10 +53,12 @@ void main() {
jsonEncode(User(id: 'testuser')), jsonEncode(User(id: 'testuser')),
], ],
status: MessageSendingStatus.sent, status: MessageSendingStatus.sent,
updatedAt: DateTime.now(), localUpdatedAt: DateTime.now(),
remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)),
extraData: {'extra_test_data': 'extraData'}, extraData: {'extra_test_data': 'extraData'},
userId: user.id, userId: user.id,
deletedAt: DateTime.now(), localDeletedAt: DateTime.now(),
remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)),
messageText: 'Hello', messageText: 'Hello',
pinned: true, pinned: true,
pinExpires: DateTime.now().toUtc(), pinExpires: DateTime.now().toUtc(),
@@ -81,7 +84,8 @@ void main() {
expect(message.parentId, entity.parentId); expect(message.parentId, entity.parentId);
expect(message.quotedMessageId, entity.quotedMessageId); expect(message.quotedMessageId, entity.quotedMessageId);
expect(message.command, entity.command); expect(message.command, entity.command);
expect(message.createdAt, isSameDateAs(entity.createdAt)); expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt));
expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt));
expect(message.shadowed, entity.shadowed); expect(message.shadowed, entity.shadowed);
expect(message.showInChannel, entity.showInChannel); expect(message.showInChannel, entity.showInChannel);
for (var i = 0; i < message.mentionedUsers.length; i++) { for (var i = 0; i < message.mentionedUsers.length; i++) {
@@ -93,10 +97,12 @@ void main() {
expect(message.reactionScores, entity.reactionScores); expect(message.reactionScores, entity.reactionScores);
expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionCounts, entity.reactionCounts);
expect(message.status, entity.status); expect(message.status, entity.status);
expect(message.updatedAt, isSameDateAs(entity.updatedAt)); expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt));
expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt));
expect(message.extraData, entity.extraData); expect(message.extraData, entity.extraData);
expect(message.user!.id, entity.userId); expect(message.user!.id, entity.userId);
expect(message.deletedAt, isSameDateAs(entity.deletedAt)); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt));
expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt));
expect(message.text, entity.messageText); expect(message.text, entity.messageText);
expect(message.pinned, entity.pinned); expect(message.pinned, entity.pinned);
expect(message.pinExpires, isSameDateAs(entity.pinExpires)); expect(message.pinExpires, isSameDateAs(entity.pinExpires));
@@ -144,7 +150,8 @@ void main() {
parentId: 'testParentId', parentId: 'testParentId',
quotedMessageId: quotedMessage.id, quotedMessageId: quotedMessage.id,
command: 'testCommand', command: 'testCommand',
createdAt: DateTime.now(), localCreatedAt: DateTime.now(),
createdAt: DateTime.now().add(const Duration(seconds: 1)),
shadowed: math.Random().nextBool(), shadowed: math.Random().nextBool(),
showInChannel: math.Random().nextBool(), showInChannel: math.Random().nextBool(),
replyCount: 33, replyCount: 33,
@@ -157,10 +164,12 @@ void main() {
(prev, curr) => (prev, curr) =>
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
), ),
updatedAt: DateTime.now(), localUpdatedAt: DateTime.now(),
updatedAt: DateTime.now().add(const Duration(seconds: 1)),
extraData: const {'extra_test_data': 'extraData'}, extraData: const {'extra_test_data': 'extraData'},
user: user, user: user,
deletedAt: DateTime.now(), localDeletedAt: DateTime.now(),
deletedAt: DateTime.now().add(const Duration(seconds: 1)),
text: 'Hello', text: 'Hello',
pinned: true, pinned: true,
pinExpires: DateTime.now(), pinExpires: DateTime.now(),
@@ -179,7 +188,8 @@ void main() {
expect(entity.parentId, message.parentId); expect(entity.parentId, message.parentId);
expect(entity.quotedMessageId, message.quotedMessageId); expect(entity.quotedMessageId, message.quotedMessageId);
expect(entity.command, message.command); expect(entity.command, message.command);
expect(entity.createdAt, isSameDateAs(message.createdAt)); expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt));
expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt));
expect(entity.shadowed, message.shadowed); expect(entity.shadowed, message.shadowed);
expect(entity.showInChannel, message.showInChannel); expect(entity.showInChannel, message.showInChannel);
expect(entity.replyCount, message.replyCount); expect(entity.replyCount, message.replyCount);
@@ -188,10 +198,12 @@ void main() {
expect(entity.reactionScores, message.reactionScores); expect(entity.reactionScores, message.reactionScores);
expect(entity.reactionCounts, message.reactionCounts); expect(entity.reactionCounts, message.reactionCounts);
expect(entity.status, message.status); expect(entity.status, message.status);
expect(entity.updatedAt, isSameDateAs(message.updatedAt)); expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt));
expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt));
expect(entity.extraData, message.extraData); expect(entity.extraData, message.extraData);
expect(entity.userId, message.user!.id); expect(entity.userId, message.user!.id);
expect(entity.deletedAt, isSameDateAs(message.deletedAt)); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt));
expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt));
expect(entity.messageText, message.text); expect(entity.messageText, message.text);
expect(entity.pinned, message.pinned); expect(entity.pinned, message.pinned);
expect(entity.pinExpires, isSameDateAs(message.pinExpires)); expect(entity.pinExpires, isSameDateAs(message.pinExpires));
@@ -38,7 +38,8 @@ void main() {
parentId: 'testParentId', parentId: 'testParentId',
quotedMessageId: quotedMessage.id, quotedMessageId: quotedMessage.id,
command: 'testCommand', command: 'testCommand',
createdAt: DateTime.now(), localCreatedAt: DateTime.now(),
remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)),
shadowed: math.Random().nextBool(), shadowed: math.Random().nextBool(),
showInChannel: math.Random().nextBool(), showInChannel: math.Random().nextBool(),
replyCount: 33, replyCount: 33,
@@ -48,12 +49,16 @@ void main() {
(prev, curr) => (prev, curr) =>
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
), ),
mentionedUsers: [], mentionedUsers: [
jsonEncode(User(id: 'testuser')),
],
status: MessageSendingStatus.sent, status: MessageSendingStatus.sent,
updatedAt: DateTime.now(), localUpdatedAt: DateTime.now(),
remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)),
extraData: {'extra_test_data': 'extraData'}, extraData: {'extra_test_data': 'extraData'},
userId: user.id, userId: user.id,
deletedAt: DateTime.now(), localDeletedAt: DateTime.now(),
remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)),
messageText: 'Hello', messageText: 'Hello',
pinned: true, pinned: true,
pinExpires: DateTime.now().toUtc(), pinExpires: DateTime.now().toUtc(),
@@ -79,17 +84,25 @@ void main() {
expect(message.parentId, entity.parentId); expect(message.parentId, entity.parentId);
expect(message.quotedMessageId, entity.quotedMessageId); expect(message.quotedMessageId, entity.quotedMessageId);
expect(message.command, entity.command); expect(message.command, entity.command);
expect(message.createdAt, isSameDateAs(entity.createdAt)); expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt));
expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt));
expect(message.shadowed, entity.shadowed); expect(message.shadowed, entity.shadowed);
expect(message.showInChannel, entity.showInChannel); expect(message.showInChannel, entity.showInChannel);
for (var i = 0; i < message.mentionedUsers.length; i++) {
final entityMentionedUser =
User.fromJson(jsonDecode(entity.mentionedUsers[i]));
expect(message.mentionedUsers[i].id, entityMentionedUser.id);
}
expect(message.replyCount, entity.replyCount); expect(message.replyCount, entity.replyCount);
expect(message.reactionScores, entity.reactionScores); expect(message.reactionScores, entity.reactionScores);
expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionCounts, entity.reactionCounts);
expect(message.status, entity.status); expect(message.status, entity.status);
expect(message.updatedAt, isSameDateAs(entity.updatedAt)); expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt));
expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt));
expect(message.extraData, entity.extraData); expect(message.extraData, entity.extraData);
expect(message.user!.id, entity.userId); expect(message.user!.id, entity.userId);
expect(message.deletedAt, isSameDateAs(entity.deletedAt)); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt));
expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt));
expect(message.text, entity.messageText); expect(message.text, entity.messageText);
expect(message.pinned, entity.pinned); expect(message.pinned, entity.pinned);
expect(message.pinExpires, isSameDateAs(entity.pinExpires)); expect(message.pinExpires, isSameDateAs(entity.pinExpires));
@@ -108,7 +121,7 @@ void main() {
} }
}); });
test('toPinnedEntity should map message into PinnedMessageEntity', () { test('toEntity should map message into MessageEntity', () {
const cid = 'testCid'; const cid = 'testCid';
final user = User(id: 'testUserId'); final user = User(id: 'testUserId');
final quotedMessage = Message(id: 'testQuotedMessageId'); final quotedMessage = Message(id: 'testQuotedMessageId');
@@ -137,20 +150,26 @@ void main() {
parentId: 'testParentId', parentId: 'testParentId',
quotedMessageId: quotedMessage.id, quotedMessageId: quotedMessage.id,
command: 'testCommand', command: 'testCommand',
createdAt: DateTime.now(), localCreatedAt: DateTime.now(),
createdAt: DateTime.now().add(const Duration(seconds: 1)),
shadowed: math.Random().nextBool(), shadowed: math.Random().nextBool(),
showInChannel: math.Random().nextBool(), showInChannel: math.Random().nextBool(),
replyCount: 33, replyCount: 33,
mentionedUsers: [
User(id: 'testuser'),
],
reactionScores: {for (final r in reactions) r.type: r.score}, reactionScores: {for (final r in reactions) r.type: r.score},
reactionCounts: reactions.fold( reactionCounts: reactions.fold(
{}, {},
(prev, curr) => (prev, curr) =>
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
), ),
updatedAt: DateTime.now(), localUpdatedAt: DateTime.now(),
updatedAt: DateTime.now().add(const Duration(seconds: 1)),
extraData: const {'extra_test_data': 'extraData'}, extraData: const {'extra_test_data': 'extraData'},
user: user, user: user,
deletedAt: DateTime.now(), localDeletedAt: DateTime.now(),
deletedAt: DateTime.now().add(const Duration(seconds: 1)),
text: 'Hello', text: 'Hello',
pinned: true, pinned: true,
pinExpires: DateTime.now(), pinExpires: DateTime.now(),
@@ -169,17 +188,22 @@ void main() {
expect(entity.parentId, message.parentId); expect(entity.parentId, message.parentId);
expect(entity.quotedMessageId, message.quotedMessageId); expect(entity.quotedMessageId, message.quotedMessageId);
expect(entity.command, message.command); expect(entity.command, message.command);
expect(entity.createdAt, isSameDateAs(message.createdAt)); expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt));
expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt));
expect(entity.shadowed, message.shadowed); expect(entity.shadowed, message.shadowed);
expect(entity.showInChannel, message.showInChannel); expect(entity.showInChannel, message.showInChannel);
expect(entity.replyCount, message.replyCount); expect(entity.replyCount, message.replyCount);
expect(
entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList());
expect(entity.reactionScores, message.reactionScores); expect(entity.reactionScores, message.reactionScores);
expect(entity.reactionCounts, message.reactionCounts); expect(entity.reactionCounts, message.reactionCounts);
expect(entity.status, message.status); expect(entity.status, message.status);
expect(entity.updatedAt, isSameDateAs(message.updatedAt)); expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt));
expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt));
expect(entity.extraData, message.extraData); expect(entity.extraData, message.extraData);
expect(entity.userId, message.user!.id); expect(entity.userId, message.user!.id);
expect(entity.deletedAt, isSameDateAs(message.deletedAt)); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt));
expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt));
expect(entity.messageText, message.text); expect(entity.messageText, message.text);
expect(entity.pinned, message.pinned); expect(entity.pinned, message.pinned);
expect(entity.pinExpires, isSameDateAs(message.pinExpires)); expect(entity.pinExpires, isSameDateAs(message.pinExpires));