Merge pull request #1584 from GetStream/release/v6.2.0

This commit is contained in:
Sahil Kumar
2023-06-02 15:58:20 +05:30
committed by GitHub
90 changed files with 635 additions and 480 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Dart Code Metrics name: Dart Code Metrics
env: env:
flutter_version: "3.7.0" flutter_version: "3.10.0"
folders: "lib, test" folders: "lib, test"
on: on:
@@ -2,7 +2,7 @@ name: stream_flutter_workflow
env: env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
flutter_version: "3.7.0" flutter_version: "3.10.0"
on: on:
pull_request: pull_request:
+16
View File
@@ -0,0 +1,16 @@
# Reporting a Vulnerability
At Stream we are committed to the security of our Software. We appreciate your efforts in disclosing vulnerabilities responsibly and we will make every effort to acknowledge your contributions.
Report security vulnerabilities at the following email address:
```
[[email protected]](mailto:[email protected])
```
Alternatively it is also possible to open a new issue in the affected repository, tagging it with the `security` tag.
A team member will acknowledge the vulnerability and will follow-up with more detailed information. A representative of the security team will be in touch if more information is needed.
# Information to include in a report
While we appreciate any information that you are willing to provide, please make sure to include the following:
* Which repository is affected
* Which branch, if relevant
* Be as descriptive as possible, the team will replicate the vulnerability before working on a fix.
+2 -2
View File
@@ -26,9 +26,9 @@ scripts:
- Note: you can also rely on your IDEs Dart Analysis / Issues window. - Note: you can also rely on your IDEs Dart Analysis / Issues window.
format: format:
run: flutter format --set-exit-if-changed . run: dart format --set-exit-if-changed .
description: | description: |
Run `flutter format --set-exit-if-changed .` in all packages. Run `dart format --set-exit-if-changed .` in all packages.
metrics: metrics:
run: | run: |
+11
View File
@@ -1,3 +1,14 @@
## 6.2.0
🐞 Fixed
- [[#1422]](https://github.com/GetStream/stream-chat-flutter/issues/1422) Fixed `User.createdAt` property using
currentTime when the ws connection is not established.
✅ Added
- Added support for `ChatPersistenceClient.isConnected` for checking if the client is connected to the database.
## 6.1.0 ## 6.1.0
🐞 Fixed 🐞 Fixed
@@ -35,8 +35,8 @@ class User extends Equatable {
this.role, this.role,
String? name, String? name,
String? image, String? image,
DateTime? createdAt, this.createdAt,
DateTime? updatedAt, this.updatedAt,
this.lastActive, this.lastActive,
Map<String, Object?> extraData = const {}, Map<String, Object?> extraData = const {},
this.online = false, this.online = false,
@@ -44,8 +44,7 @@ class User extends Equatable {
this.banExpires, this.banExpires,
this.teams = const [], this.teams = const [],
this.language, this.language,
}) : createdAt = createdAt ?? DateTime.now(), }) :
updatedAt = updatedAt ?? DateTime.now(),
// For backwards compatibility, set 'name', 'image' in [extraData]. // For backwards compatibility, set 'name', 'image' in [extraData].
extraData = { extraData = {
...extraData, ...extraData,
@@ -104,11 +103,11 @@ class User extends Equatable {
/// Date of user creation. /// Date of user creation.
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
final DateTime createdAt; final DateTime? createdAt;
/// Date of last user update. /// Date of last user update.
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
final DateTime updatedAt; final DateTime? updatedAt;
/// Date of last user connection. /// Date of last user connection.
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
@@ -14,6 +14,9 @@ import 'package:stream_chat/src/core/util/extension.dart';
/// A simple client used for persisting chat data locally. /// A simple client used for persisting chat data locally.
abstract class ChatPersistenceClient { abstract class ChatPersistenceClient {
/// Whether the connection is established.
bool get isConnected;
/// Creates a new connection to the client /// Creates a new connection to the client
Future<void> connect(String userId); Future<void> connect(String userId);
+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.1.0'; const PACKAGE_VERSION = '6.2.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.1.0 version: 6.2.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
@@ -198,8 +198,8 @@ void main() {
expect(user.banned, false); expect(user.banned, false);
expect(user.teams, []); expect(user.teams, []);
expect(user.lastActive, null); expect(user.lastActive, null);
expect(user.createdAt, isNotNull); expect(user.createdAt, null);
expect(user.updatedAt, isNotNull); expect(user.updatedAt, null);
}); });
test('default values, parse json', () { test('default values, parse json', () {
@@ -214,8 +214,8 @@ void main() {
expect(user.banned, false); expect(user.banned, false);
expect(user.teams, []); expect(user.teams, []);
expect(user.lastActive, null); expect(user.lastActive, null);
expect(user.createdAt, isNotNull); expect(user.createdAt, null);
expect(user.updatedAt, isNotNull); expect(user.updatedAt, null);
}); });
}); });
} }
@@ -12,6 +12,9 @@ import 'package:stream_chat/src/db/chat_persistence_client.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class TestPersistenceClient extends ChatPersistenceClient { class TestPersistenceClient extends ChatPersistenceClient {
@override
bool get isConnected => throw UnimplementedError();
@override @override
Future<void> connect(String userId) => throw UnimplementedError(); Future<void> connect(String userId) => throw UnimplementedError();
+40
View File
@@ -1,3 +1,43 @@
## 6.2.0
🐞 Fixed
- [[#1546]](https://github.com/GetStream/stream-chat-flutter/issues/1546)
Fixed `StreamMessageInputTheme.linkHighlightColor` returning null for default theme.
- [[#1548]](https://github.com/GetStream/stream-chat-flutter/issues/1548) Fixed `StreamMessageInput` urlRegex only
matching the lowercase `http(s)|ftp`.
- [[#1542]](https://github.com/GetStream/stream-chat-flutter/issues/1542) Handle error thrown in `StreamMessageInput`
when unable to fetch a link preview.
- [[#1540]](https://github.com/GetStream/stream-chat-flutter/issues/1540) Use `CircularProgressIndicator.adaptive`
instead of material indicator.
- [[#1490]](https://github.com/GetStream/stream-chat-flutter/issues/1490) Fixed `editMessageInputBuilder` property not
used in `MessageActionsModal.editMessage` option.
- [[#1544]](https://github.com/GetStream/stream-chat-flutter/issues/1544) Fixed error thrown when unable to fetch
image/data in Message link preview.
- [[#1482]](https://github.com/GetStream/stream-chat-flutter/issues/1482) Fixed `StreaChannelListTile` not showing
unread indicator when `currentUser` is not present in the initial member list.
- [[#1487]](https://github.com/GetStream/stream-chat-flutter/issues/1487) Use localized title
for `WebOrDesktopAttachmentPickerOption` in `StreamMessageInput`.
- [[#1250]](https://github.com/GetStream/stream-chat-flutter/issues/1250) Fixed bottomRow widgetSpans getting resized
twice when `textScaling` is enabled.
- [[#1498]](https://github.com/GetStream/stream-chat-flutter/issues/1498) Fixed `MessageInput` autocomplete not working
on non-mobile platforms.
- [[#1576]](https://github.com/GetStream/stream-chat-flutter/issues/1576) Temporary fix for `StreamMessageListView`
getting broken when loaded at a particular message and a new message is added.
✅ Added
- Added support for `StreamMessageThemeData.urlAttachmentTextMaxLine` to specify the `.maxLines` for the url attachment
text. [#1543](https://github.com/GetStream/stream-chat-flutter/issues/1543)
🔄 Changed
- Updated `shimmer` dependency to `^3.0.0`.
- Updated `image_gallery_saver` dependency to `^2.0.1`.
- Deprecated `ChannelPreview` in favor of `StreamChannelListTile`.
- Updated `stream_chat_flutter_core` dependency
to [`6.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
## 6.1.0 ## 6.1.0
🐞 Fixed 🐞 Fixed
+1 -1
View File
@@ -196,7 +196,7 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart';
final chatPersistentClient = StreamChatPersistenceClient( final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO, logLevel: Level.INFO,
connectionMode: ConnectionMode.background, connectionMode: ConnectionMode.regular,
); );
final client = StreamChatClient( final client = StreamChatClient(
@@ -128,12 +128,12 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Navigator( Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute( onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold( builder: (context) => const Scaffold(
appBar: const StreamChannelHeader( appBar: StreamChannelHeader(
showBackButton: false, showBackButton: false,
), ),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -91,10 +91,10 @@ class ChannelPage extends StatelessWidget {
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -126,10 +126,10 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -168,10 +168,10 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -28,7 +28,7 @@ dependencies:
cupertino_icons: ^1.0.4 cupertino_icons: ^1.0.4
flutter: flutter:
sdk: flutter sdk: flutter
responsive_builder: ^0.6.4 responsive_builder: ^0.7.0
stream_chat_flutter: stream_chat_flutter:
path: ../ path: ../
stream_chat_localizations: stream_chat_localizations:
@@ -200,7 +200,7 @@ class _FileTypeImage extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
), ),
@@ -210,7 +210,7 @@ class _FileTypeImage extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
), ),
@@ -59,6 +59,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
color: StreamChatTheme.of(context).colorTheme.barsBg, color: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 2, elevation: 2,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16), topRight: Radius.circular(16),
@@ -115,7 +116,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
width: constraints?.maxHeight, width: constraints?.maxHeight,
height: constraints?.maxWidth, height: constraints?.maxWidth,
child: const Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
imageUrl: imageUrl, imageUrl: imageUrl,
@@ -241,10 +242,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
const Align( const Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: StreamVisibleFootnote(),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: StreamVisibleFootnote(),
),
), ),
], ],
), ),
@@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template streamUrlAttachment} /// {@template streamUrlAttachment}
@@ -64,14 +65,33 @@ class StreamUrlAttachment extends StatelessWidget {
), ),
child: Stack( child: Stack(
children: [ children: [
CachedNetworkImage( AspectRatio(
width: double.infinity, // Default aspect ratio for Open Graph images.
imageUrl: urlAttachment.imageUrl!, // https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
fit: BoxFit.cover, aspectRatio: 1.91 / 1,
child: CachedNetworkImage(
imageUrl: urlAttachment.imageUrl!,
fit: BoxFit.cover,
placeholder: (context, __) {
final image = Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
final colorTheme =
StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
errorWidget: (_, __, ___) => const AttachmentError(),
),
), ),
Positioned( Positioned(
left: 0, left: 0,
bottom: -1, bottom: 0,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -83,7 +103,8 @@ class StreamUrlAttachment extends StatelessWidget {
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 8, top: 8,
left: 8, left: 8,
right: 8, right: 12,
bottom: 4,
), ),
child: Text( child: Text(
hostDisplayName, hostDisplayName,
@@ -99,20 +120,40 @@ class StreamUrlAttachment extends StatelessWidget {
padding: textPadding, padding: textPadding,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
if (urlAttachment.title != null) if (urlAttachment.title != null)
Text( Builder(builder: (context) {
urlAttachment.title!.trim(), final maxLines = messageTheme.urlAttachmentTitleMaxLine;
maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1,
overflow: TextOverflow.ellipsis, TextOverflow? overflow;
style: messageTheme.urlAttachmentTitleStyle, if (maxLines != null && maxLines > 0) {
), overflow = TextOverflow.ellipsis;
}
return Text(
urlAttachment.title!.trim(),
maxLines: maxLines,
overflow: overflow,
style: messageTheme.urlAttachmentTitleStyle,
);
}),
if (urlAttachment.text != null) if (urlAttachment.text != null)
Text( Builder(builder: (context) {
urlAttachment.text!, final maxLines = messageTheme.urlAttachmentTextMaxLine;
style: messageTheme.urlAttachmentTextStyle,
), TextOverflow? overflow;
], if (maxLines != null && maxLines > 0) {
overflow = TextOverflow.ellipsis;
}
return Text(
urlAttachment.text!,
maxLines: maxLines,
overflow: overflow,
style: messageTheme.urlAttachmentTextStyle,
);
}),
].insertBetween(const SizedBox(height: 4)),
), ),
), ),
], ],
@@ -345,9 +345,11 @@ class AttachmentActionsModal extends StatelessWidget {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
CircularProgressIndicator( CircularProgressIndicator.adaptive(
strokeWidth: 8, strokeWidth: 8,
color: theme.colorTheme.accentPrimary, valueColor: AlwaysStoppedAnimation<Color>(
theme.colorTheme.accentPrimary,
),
), ),
Center( Center(
child: Text( child: Text(
@@ -507,10 +507,12 @@ class _StreamAutocompleteState extends State<StreamAutocomplete> {
final anchor = widget.optionsAlignment._toAnchor(); final anchor = widget.optionsAlignment._toAnchor();
final shouldShowOptions = _shouldShowOptions; final shouldShowOptions = _shouldShowOptions;
final optionViewBuilder = shouldShowOptions final optionViewBuilder = shouldShowOptions
? _currentTrigger!.optionsViewBuilder( ? TextFieldTapRegion(
context, child: _currentTrigger!.optionsViewBuilder(
_currentQuery!, context,
_messageEditingController, _currentQuery!,
_messageEditingController,
),
) )
: null; : null;
@@ -143,7 +143,7 @@ class _ConnectingTitleState extends StatelessWidget {
height: 16, height: 16,
width: 16, width: 16,
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -244,7 +244,7 @@ class _ConnectingTitleState extends StatelessWidget {
height: 16, height: 16,
width: 16, width: 16,
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -24,6 +24,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The UI is rendered based on the first ancestor of type [StreamChatTheme]. /// The UI is rendered based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget's appearance. /// Modify it to change the widget's appearance.
/// {@endtemplate} /// {@endtemplate}
@Deprecated('Use StreamChannelListTile instead.')
class ChannelPreview extends StatelessWidget { class ChannelPreview extends StatelessWidget {
/// {@macro channelPreview} /// {@macro channelPreview}
const ChannelPreview({ const ChannelPreview({
@@ -337,7 +337,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
final controller = videoPackages[attachment.id]!; final controller = videoPackages[attachment.id]!;
if (!controller.initialized) { if (!controller.initialized) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
); );
} }
return InkWell( return InkWell(
@@ -51,7 +51,7 @@ class StreamUploadProgressIndicator extends StatelessWidget {
SizedBox( SizedBox(
height: 16, height: 16,
width: 16, width: 16,
child: CircularProgressIndicator( child: CircularProgressIndicator.adaptive(
strokeWidth: 3, strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(progressIndicatorColor), valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
), ),
@@ -400,8 +400,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
builder: (context) => EditMessageSheet( builder: (context) => EditMessageSheet(
message: widget.message,
channel: channel, channel: channel,
message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder,
), ),
); );
} }
@@ -792,19 +792,19 @@ Widget webOrDesktopAttachmentPickerBuilder({
key: 'image-picker', key: 'image-picker',
type: AttachmentPickerType.images, type: AttachmentPickerType.images,
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
title: 'Upload a photo', title: context.translations.uploadAPhotoLabel,
), ),
WebOrDesktopAttachmentPickerOption( WebOrDesktopAttachmentPickerOption(
key: 'video-picker', key: 'video-picker',
type: AttachmentPickerType.videos, type: AttachmentPickerType.videos,
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
title: 'Upload a video', title: context.translations.uploadAVideoLabel,
), ),
WebOrDesktopAttachmentPickerOption( WebOrDesktopAttachmentPickerOption(
key: 'file-picker', key: 'file-picker',
type: AttachmentPickerType.files, type: AttachmentPickerType.files,
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
title: 'Upload a file', title: context.translations.uploadAFileLabel,
), ),
}, },
onOptionTap: (context, controller, option) async { onOptionTap: (context, controller, option) async {
@@ -303,7 +303,7 @@ class _ParseAttachments extends StatelessWidget {
width: size.width, width: size.width,
height: size.height, height: size.height,
child: const Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
); );
}, },
@@ -394,7 +394,7 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
width: 32, width: 32,
child: _controller.value.isInitialized child: _controller.value.isInitialized
? VideoPlayer(_controller) ? VideoPlayer(_controller)
: const CircularProgressIndicator(), : const CircularProgressIndicator.adaptive(),
); );
} }
} }
@@ -1042,6 +1042,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
CancelableOperation? _enrichUrlOperation; CancelableOperation? _enrichUrlOperation;
final _urlRegex = RegExp( final _urlRegex = RegExp(
r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+',
caseSensitive: false,
); );
void _checkContainsUrl(String value, BuildContext context) async { void _checkContainsUrl(String value, BuildContext context) async {
@@ -1103,8 +1104,12 @@ class StreamMessageInputState extends State<StreamMessageInput>
var response = _ogAttachmentCache[url]; var response = _ogAttachmentCache[url];
if (response == null) { if (response == null) {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
response = await client.enrichUrl(url); try {
_ogAttachmentCache[url] = response; response = await client.enrichUrl(url);
_ogAttachmentCache[url] = response;
} catch (e, stk) {
return Future.error(e, stk);
}
} }
return response; return response;
} }
@@ -1367,10 +1372,10 @@ class StreamMessageInputState extends State<StreamMessageInput>
} }
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
final channel = streamChannel.channel;
var message = _effectiveController.value; var message = _effectiveController.value;
if (!streamChannel.channel.ownCapabilities if (!channel.ownCapabilities.contains(PermissionType.sendLinks) &&
.contains(PermissionType.sendLinks) &&
_urlRegex.allMatches(message.text ?? '').any((element) => _urlRegex.allMatches(message.text ?? '').any((element) =>
element.group(0)?.split('.').last.isValidTLD() == true)) { element.group(0)?.split('.').last.isValidTLD() == true)) {
showInfoBottomSheet( showInfoBottomSheet(
@@ -1396,8 +1401,8 @@ class StreamMessageInputState extends State<StreamMessageInput>
final skipEnrichUrl = _effectiveController.ogAttachment == null; final skipEnrichUrl = _effectiveController.ogAttachment == null;
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled; shouldKeepFocus ??= !_commandEnabled;
widget.onQuotedMessageCleared?.call(); widget.onQuotedMessageCleared?.call();
_effectiveController.reset(); _effectiveController.reset();
@@ -1406,12 +1411,35 @@ class StreamMessageInputState extends State<StreamMessageInput>
message = await widget.preMessageSending!(message); message = await widget.preMessageSending!(message);
} }
final channel = streamChannel.channel; message = message.replaceMentionsWithId();
// If the channel is not up to date, we should reload it before sending
// the message.
if (!channel.state!.isUpToDate) { if (!channel.state!.isUpToDate) {
await streamChannel.reloadChannel(); await streamChannel.reloadChannel();
// We need to wait for the frame to be rendered with the updated channel
// state before sending the message.
await WidgetsBinding.instance.endOfFrame;
} }
message = message.replaceMentionsWithId(); await _sendOrUpdateMessage(
message: message,
skipEnrichUrl: skipEnrichUrl,
);
if (shouldKeepFocus) {
FocusScope.of(context).requestFocus(_effectiveFocusNode);
} else {
FocusScope.of(context).unfocus();
}
}
Future<void> _sendOrUpdateMessage({
required Message message,
bool skipEnrichUrl = false,
}) async {
final channel = StreamChannel.of(context).channel;
try { try {
Future sendingFuture; Future sendingFuture;
@@ -1427,12 +1455,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
); );
} }
if (shouldKeepFocus) {
FocusScope.of(context).requestFocus(_effectiveFocusNode);
} else {
FocusScope.of(context).unfocus();
}
final resp = await sendingFuture; final resp = await sendingFuture;
if (resp.message?.type == 'error') { if (resp.message?.type == 'error') {
_effectiveController.message = message; _effectiveController.message = message;
@@ -52,7 +52,7 @@ class LoadingIndicator extends StatelessWidget {
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
); );
}, },
@@ -432,7 +432,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
messageFilter: widget.messageFilter, messageFilter: widget.messageFilter,
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) => const Center( (context) => const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
), ),
emptyBuilder: widget.emptyBuilder ?? emptyBuilder: widget.emptyBuilder ??
(context) => Center( (context) => Center(
@@ -566,21 +566,33 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
reverse: widget.reverse, reverse: widget.reverse,
shrinkWrap: widget.shrinkWrap, shrinkWrap: widget.shrinkWrap,
itemCount: itemCount, itemCount: itemCount,
findChildIndexCallback: (Key key) {
final indexedKey = key as IndexedKey; // Commented out as it is not working as expected.
final valueKey = indexedKey.key as ValueKey<String>?; // The list view gets broken in the following case:
if (valueKey != null) { // * The list view is loaded at a particular message (eg: Last Read, or a quoted message)
final index = messagesIndex[valueKey.value]; // and a new message is added to the list view.
if (index != null) { //
// The calculation is as follows: // Issues faced:
// * Add 2 to the index retrieved to account for the footer and the bottom loader. // * https://github.com/GetStream/stream-chat-flutter/issues/1576
// * Multiply the result by 2 to account for the separators between each pair of items. // * https://github.com/GetStream/stream-chat-flutter/issues/1414
// * Subtract 1 to adjust for the 0-based indexing of the list view. //
return ((index + 2) * 2) - 1; // Related issues: https://github.com/flutter/flutter/issues/107123
} //
} // findChildIndexCallback: (Key key) {
return null; // final indexedKey = key as IndexedKey;
}, // final valueKey = indexedKey.key as ValueKey<String>?;
// if (valueKey != null) {
// final index = messagesIndex[valueKey.value];
// if (index != null) {
// // The calculation is as follows:
// // * Add 2 to the index retrieved to account for the footer and the bottom loader.
// // * Multiply the result by 2 to account for the separators between each pair of items.
// // * Subtract 1 to adjust for the 0-based indexing of the list view.
// return ((index + 2) * 2) - 1;
// }
// }
// return null;
// },
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)|
@@ -899,7 +911,10 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final hasUrlAttachment = final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null); message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null; final isEphemeral = message.isEphemeral;
final borderSide =
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final defaultMessageWidget = StreamMessageWidget( final defaultMessageWidget = StreamMessageWidget(
showReplyMessage: false, showReplyMessage: false,
@@ -980,24 +995,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
FloatingActionButton( FloatingActionButton(
backgroundColor: _streamTheme.colorTheme.barsBg, backgroundColor: _streamTheme.colorTheme.barsBg,
onPressed: () async { onPressed: () async {
if (unreadCount > 0) { return scrollToBottomDefaultTapAction(unreadCount);
streamChannel!.channel.markRead();
}
if (!_upToDate) {
_bottomPaginationActive = false;
initialAlignment = 0;
initialIndex = 0;
await streamChannel!.reloadChannel();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController!.jumpTo(index: 0);
});
} else {
_showScrollToBottom.value = false;
_scrollController!.jumpTo(
index: 0,
);
}
}, },
child: widget.reverse child: widget.reverse
? StreamSvgIcon.down( ? StreamSvgIcon.down(
@@ -1101,10 +1099,13 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final isEphemeral = message.isEphemeral;
final hasUrlAttachment = final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null); message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null; final borderSide =
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final currentUser = StreamChat.of(context).currentUser; final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? []; final members = StreamChannel.of(context).channel.state?.members ?? [];
@@ -153,8 +153,6 @@ class BottomRow extends StatelessWidget {
} }
} }
final children = <WidgetSpan>[];
final threadParticipants = message.threadParticipants?.take(2); final threadParticipants = message.threadParticipants?.take(2);
final showThreadParticipants = threadParticipants?.isNotEmpty == true; final showThreadParticipants = threadParticipants?.isNotEmpty == true;
final replyCount = message.replyCount; final replyCount = message.replyCount;
@@ -183,75 +181,69 @@ class BottomRow extends StatelessWidget {
const usernameKey = Key('username'); const usernameKey = Key('username');
children.addAll([ final children = [
if (showUsername) if (showUsername)
WidgetSpan( usernameBuilder?.call(context, message) ??
child: usernameBuilder?.call(context, message) ?? Username(
Username( key: usernameKey,
key: usernameKey, message: message,
message: message, messageTheme: messageTheme,
messageTheme: messageTheme, ),
),
),
if (showTimeStamp) if (showTimeStamp)
WidgetSpan( Text(
child: Text( Jiffy(message.createdAt.toLocal()).jm,
Jiffy(message.createdAt.toLocal()).jm, style: messageTheme.createdAtStyle,
style: messageTheme.createdAtStyle,
),
), ),
if (showSendingIndicator) if (showSendingIndicator)
WidgetSpan( sendingIndicatorBuilder?.call(context, message) ??
child: sendingIndicatorBuilder?.call(context, message) ?? SendingIndicatorBuilder(
SendingIndicatorBuilder( messageTheme: messageTheme,
messageTheme: messageTheme, message: message,
message: message, hasNonUrlAttachments: hasNonUrlAttachments,
hasNonUrlAttachments: hasNonUrlAttachments, streamChat: streamChat,
streamChat: streamChat, streamChatTheme: streamChatTheme,
streamChatTheme: streamChatTheme, ),
), ];
),
]);
final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) && final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) &&
(showThreadReplyIndicator || showInChannel); (showThreadReplyIndicator || showInChannel);
final threadIndicatorWidgets = <WidgetSpan>[ final threadIndicatorWidgets = [
if (showThreadTail) if (showThreadTail)
WidgetSpan( // Added builder to use the nearest context to get the right
child: Padding( // textScaleFactor value.
padding: EdgeInsets.only( Builder(
bottom: context.textScaleFactor * builder: (context) {
((messageTheme.repliesStyle?.fontSize ?? 1) / 2), return Padding(
), padding: EdgeInsets.only(
child: CustomPaint( bottom: context.textScaleFactor *
size: const Size(16, 32) * context.textScaleFactor, ((messageTheme.repliesStyle?.fontSize ?? 1) / 2),
painter: ThreadReplyPainter(
context: context,
color: messageTheme.messageBorderColor,
reverse: reverse,
), ),
), child: CustomPaint(
), size: const Size(16, 32) * context.textScaleFactor,
painter: ThreadReplyPainter(
context: context,
color: messageTheme.messageBorderColor,
reverse: reverse,
),
),
);
},
), ),
if (showInChannel || showThreadReplyIndicator) ...[ if (showInChannel || showThreadReplyIndicator) ...[
if (showThreadParticipants) if (showThreadParticipants)
WidgetSpan( SizedBox.fromSize(
child: SizedBox.fromSize( size: Size((threadParticipants!.length * 8.0) + 8, 16),
size: Size((threadParticipants!.length * 8.0) + 8, 16), child: ThreadParticipants(
child: ThreadParticipants( threadParticipants: threadParticipants,
threadParticipants: threadParticipants, streamChatTheme: streamChatTheme,
streamChatTheme: streamChatTheme,
),
), ),
), ),
WidgetSpan( MouseRegion(
child: MouseRegion( cursor: SystemMouseCursors.click,
cursor: SystemMouseCursors.click, child: GestureDetector(
child: GestureDetector( onTap: _onThreadTap,
onTap: _onThreadTap, child: Text(msg, style: messageTheme.repliesStyle),
child: Text(msg, style: messageTheme.repliesStyle),
),
), ),
), ),
], ],
@@ -266,8 +258,21 @@ class BottomRow extends StatelessWidget {
return Text.rich( return Text.rich(
TextSpan( TextSpan(
children: [ children: [
...children, ...children.insertBetween(const SizedBox(width: 8)).map((child) {
].insertBetween(const WidgetSpan(child: SizedBox(width: 8))), final mediaQueryData = MediaQuery.of(context);
return WidgetSpan(
child: MediaQuery(
// Hardcoding the textScaleFactor to 1 to avoid the multiple
// resizing of the text. This is needed because the
// textScaleFactor is already applied to the textSpan.
//
// issue: https://github.com/GetStream/stream-chat-flutter/issues/1250
data: mediaQueryData.copyWith(textScaleFactor: 1),
child: child,
),
);
}),
],
), ),
maxLines: 1, maxLines: 1,
textAlign: reverse ? TextAlign.right : TextAlign.left, textAlign: reverse ? TextAlign.right : TextAlign.left,
@@ -217,17 +217,9 @@ class StreamMessageWidget extends StatefulWidget {
); );
}, },
'giphy': (context, message, attachments) { 'giphy': (context, message, attachments) {
final border = RoundedRectangleBorder( final attachmentWidget = Column(
side: attachmentBorderSide ?? children: [
BorderSide( ...attachments.map((attachment) {
color: StreamChatTheme.of(context).colorTheme.borders,
),
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
);
return WrapAttachmentWidget(
attachmentWidget: Column(
children: attachments.map((attachment) {
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
return StreamGiphyAttachment( return StreamGiphyAttachment(
attachment: attachment, attachment: attachment,
@@ -240,14 +232,25 @@ class StreamMessageWidget extends StatefulWidget {
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
onReplyMessage: onReplyTap, onReplyMessage: onReplyTap,
onAttachmentTap: onAttachmentTap != null onAttachmentTap: onAttachmentTap != null
? () { ? () => onAttachmentTap(message, attachment)
onAttachmentTap(message, attachment);
}
: null, : null,
); );
}).toList(), }),
), ],
);
// If the message is ephemeral, we don't want to show the border.
if (message.isEphemeral) return attachmentWidget;
final color = StreamChatTheme.of(context).colorTheme.borders;
final border = RoundedRectangleBorder(
side: attachmentBorderSide ?? BorderSide(color: color),
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
);
return WrapAttachmentWidget(
attachmentShape: border, attachmentShape: border,
attachmentWidget: attachmentWidget,
); );
}, },
'file': (context, message, attachments) { 'file': (context, message, attachments) {
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
/// {@endtemplate} /// {@endtemplate}
typedef ReactionIconBuilder = Widget Function( typedef ReactionIconBuilder = Widget Function(
BuildContext context, BuildContext context,
// ignore: avoid_positional_boolean_parameters
bool isHighlighted, bool isHighlighted,
double iconSize, double iconSize,
); );
@@ -196,8 +196,7 @@ class StreamChannelListTile extends StatelessWidget {
initialData: channelState.members, initialData: channelState.members,
comparator: const ListEquality().equals, comparator: const ListEquality().equals,
builder: (context, members) { builder: (context, members) {
if (members.isEmpty || if (members.isEmpty) {
!members.any((it) => it.user!.id == currentUser.id)) {
return const Offstage(); return const Offstage();
} }
return unreadIndicatorBuilder?.call(context) ?? return unreadIndicatorBuilder?.call(context) ??
@@ -380,41 +380,3 @@ class StreamChannelListSeparator extends StatelessWidget {
); );
} }
} }
/// A widget that is used to display an error screen
/// when [StreamChannelListController] fails to load initial channels.
class StreamChannelListErrorWidget extends StatelessWidget {
/// Creates a new instance of [StreamChannelListErrorWidget] widget.
const StreamChannelListErrorWidget({
super.key,
this.onPressed,
});
/// The callback to invoke when the user taps on the retry button.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(right: 2),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: onPressed,
child: Text(context.translations.retryLabel),
),
],
);
}
@@ -202,9 +202,9 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
} }
@override @override
ImageStreamCompleter loadBuffer( ImageStreamCompleter loadImage(
MediaThumbnailProvider key, MediaThumbnailProvider key,
DecoderBufferCallback decode, ImageDecoderCallback decode,
) { ) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
@@ -221,7 +221,7 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
Future<ui.Codec> _loadAsync( Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key, MediaThumbnailProvider key,
DecoderBufferCallback decode, ImageDecoderCallback decode,
) async { ) async {
assert(key == this, '$key is not $this'); assert(key == this, '$key is not $this');
final bytes = await media.thumbnailDataWithSize( final bytes = await media.thumbnailDataWithSize(
@@ -66,7 +66,7 @@ class StreamColorTheme {
this.appBg = const Color(0xff070A0D), this.appBg = const Color(0xff070A0D),
this.barsBg = const Color(0xff101418), this.barsBg = const Color(0xff101418),
this.linkBg = const Color(0xff00193D), this.linkBg = const Color(0xff00193D),
this.accentPrimary = const Color(0xff005FFF), this.accentPrimary = const Color(0xff337eff),
this.accentError = const Color(0xffFF3742), this.accentError = const Color(0xffFF3742),
this.accentInfo = const Color(0xff20E070), this.accentInfo = const Color(0xff20E070),
this.borderTop = const Effect( this.borderTop = const Effect(
@@ -153,6 +153,7 @@ class StreamMessageInputThemeData with Diagnosticable {
sendButtonColor: sendButtonColor ?? this.sendButtonColor, sendButtonColor: sendButtonColor ?? this.sendButtonColor,
actionButtonIdleColor: actionButtonIdleColor:
actionButtonIdleColor ?? this.actionButtonIdleColor, actionButtonIdleColor ?? this.actionButtonIdleColor,
linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor,
expandButtonColor: expandButtonColor ?? this.expandButtonColor, expandButtonColor: expandButtonColor ?? this.expandButtonColor,
inputTextStyle: inputTextStyle ?? this.inputTextStyle, inputTextStyle: inputTextStyle ?? this.inputTextStyle,
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
@@ -27,6 +29,7 @@ class StreamMessageThemeData with Diagnosticable {
this.urlAttachmentTitleStyle, this.urlAttachmentTitleStyle,
this.urlAttachmentTextStyle, this.urlAttachmentTextStyle,
this.urlAttachmentTitleMaxLine, this.urlAttachmentTitleMaxLine,
this.urlAttachmentTextMaxLine,
}) : urlAttachmentBackgroundColor = }) : urlAttachmentBackgroundColor =
urlAttachmentBackgroundColor ?? linkBackgroundColor; urlAttachmentBackgroundColor ?? linkBackgroundColor;
@@ -82,6 +85,9 @@ class StreamMessageThemeData with Diagnosticable {
/// Max number of lines in Url link title. /// Max number of lines in Url link title.
final int? urlAttachmentTitleMaxLine; final int? urlAttachmentTitleMaxLine;
/// Max number of lines in Url link text.
final int? urlAttachmentTextMaxLine;
/// Copy with a theme /// Copy with a theme
StreamMessageThemeData copyWith({ StreamMessageThemeData copyWith({
TextStyle? messageTextStyle, TextStyle? messageTextStyle,
@@ -102,6 +108,7 @@ class StreamMessageThemeData with Diagnosticable {
TextStyle? urlAttachmentTitleStyle, TextStyle? urlAttachmentTitleStyle,
TextStyle? urlAttachmentTextStyle, TextStyle? urlAttachmentTextStyle,
int? urlAttachmentTitleMaxLine, int? urlAttachmentTitleMaxLine,
int? urlAttachmentTextMaxLine,
}) { }) {
return StreamMessageThemeData( return StreamMessageThemeData(
messageTextStyle: messageTextStyle ?? this.messageTextStyle, messageTextStyle: messageTextStyle ?? this.messageTextStyle,
@@ -128,6 +135,8 @@ class StreamMessageThemeData with Diagnosticable {
urlAttachmentTextStyle ?? this.urlAttachmentTextStyle, urlAttachmentTextStyle ?? this.urlAttachmentTextStyle,
urlAttachmentTitleMaxLine: urlAttachmentTitleMaxLine:
urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine, urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine,
urlAttachmentTextMaxLine:
urlAttachmentTextMaxLine ?? this.urlAttachmentTextMaxLine,
); );
} }
@@ -178,6 +187,16 @@ class StreamMessageThemeData with Diagnosticable {
b.urlAttachmentTitleStyle, b.urlAttachmentTitleStyle,
t, t,
), ),
urlAttachmentTitleMaxLine: lerpDouble(
a.urlAttachmentTitleMaxLine,
b.urlAttachmentTitleMaxLine,
t,
)?.round(),
urlAttachmentTextMaxLine: lerpDouble(
a.urlAttachmentTextMaxLine,
b.urlAttachmentTextMaxLine,
t,
)?.round(),
); );
} }
@@ -206,6 +225,7 @@ class StreamMessageThemeData with Diagnosticable {
urlAttachmentTitleStyle: other.urlAttachmentTitleStyle, urlAttachmentTitleStyle: other.urlAttachmentTitleStyle,
urlAttachmentTextStyle: other.urlAttachmentTextStyle, urlAttachmentTextStyle: other.urlAttachmentTextStyle,
urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine, urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine,
urlAttachmentTextMaxLine: other.urlAttachmentTextMaxLine,
); );
} }
@@ -229,7 +249,8 @@ class StreamMessageThemeData with Diagnosticable {
urlAttachmentHostStyle == other.urlAttachmentHostStyle && urlAttachmentHostStyle == other.urlAttachmentHostStyle &&
urlAttachmentTitleStyle == other.urlAttachmentTitleStyle && urlAttachmentTitleStyle == other.urlAttachmentTitleStyle &&
urlAttachmentTextStyle == other.urlAttachmentTextStyle && urlAttachmentTextStyle == other.urlAttachmentTextStyle &&
urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine; urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine &&
urlAttachmentTextMaxLine == other.urlAttachmentTextMaxLine;
@override @override
int get hashCode => int get hashCode =>
@@ -248,7 +269,8 @@ class StreamMessageThemeData with Diagnosticable {
urlAttachmentHostStyle.hashCode ^ urlAttachmentHostStyle.hashCode ^
urlAttachmentTitleStyle.hashCode ^ urlAttachmentTitleStyle.hashCode ^
urlAttachmentTextStyle.hashCode ^ urlAttachmentTextStyle.hashCode ^
urlAttachmentTitleMaxLine.hashCode; urlAttachmentTitleMaxLine.hashCode ^
urlAttachmentTextMaxLine.hashCode;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -284,6 +306,10 @@ class StreamMessageThemeData with Diagnosticable {
..add(DiagnosticsProperty( ..add(DiagnosticsProperty(
'urlAttachmentTitleMaxLine', 'urlAttachmentTitleMaxLine',
urlAttachmentTitleMaxLine, urlAttachmentTitleMaxLine,
))
..add(DiagnosticsProperty(
'urlAttachmentTextMaxLine',
urlAttachmentTextMaxLine,
)); ));
} }
} }
@@ -195,15 +195,12 @@ class StreamChatThemeData {
width: 32, width: 32,
), ),
), ),
messageLinksStyle: TextStyle( messageLinksStyle: TextStyle(color: accentColor),
color: accentColor,
),
urlAttachmentBackgroundColor: colorTheme.linkBg, urlAttachmentBackgroundColor: colorTheme.linkBg,
urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor), urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor),
urlAttachmentTitleStyle: urlAttachmentTitleStyle: textTheme.footnoteBold,
textTheme.body.copyWith(fontWeight: FontWeight.w700), urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTextStyle: urlAttachmentTitleMaxLine: 1,
textTheme.body.copyWith(fontWeight: FontWeight.w400),
), ),
otherMessageTheme: StreamMessageThemeData( otherMessageTheme: StreamMessageThemeData(
reactionsBackgroundColor: colorTheme.borders, reactionsBackgroundColor: colorTheme.borders,
@@ -215,9 +212,7 @@ class StreamChatThemeData {
messageAuthorStyle: messageAuthorStyle:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor),
messageLinksStyle: TextStyle( messageLinksStyle: TextStyle(color: accentColor),
color: accentColor,
),
messageBackgroundColor: colorTheme.barsBg, messageBackgroundColor: colorTheme.barsBg,
messageBorderColor: colorTheme.borders, messageBorderColor: colorTheme.borders,
avatarTheme: StreamAvatarThemeData( avatarTheme: StreamAvatarThemeData(
@@ -229,10 +224,9 @@ class StreamChatThemeData {
), ),
urlAttachmentBackgroundColor: colorTheme.linkBg, urlAttachmentBackgroundColor: colorTheme.linkBg,
urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor), urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor),
urlAttachmentTitleStyle: urlAttachmentTitleStyle: textTheme.footnoteBold,
textTheme.body.copyWith(fontWeight: FontWeight.w700), urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTextStyle: urlAttachmentTitleMaxLine: 1,
textTheme.body.copyWith(fontWeight: FontWeight.w400),
), ),
messageInputTheme: StreamMessageInputThemeData( messageInputTheme: StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -4,7 +4,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
bool get isWeb => CurrentPlatform.isWeb; bool get isWeb => CurrentPlatform.isWeb;
/// Returns true if the app is running in a mobile device. /// Returns true if the app is running in a mobile device.
bool get isMobileDevice => true; bool get isMobileDevice => CurrentPlatform.isIos || CurrentPlatform.isAndroid;
/// Returns true if the app is running in a desktop device. /// Returns true if the app is running in a desktop device.
bool get isDesktopDevice => bool get isDesktopDevice =>
@@ -22,7 +22,7 @@ bool get isDesktopVideoPlayerSupported =>
bool get isMobileDeviceOrWeb => isWeb || isMobileDevice; bool get isMobileDeviceOrWeb => isWeb || isMobileDevice;
/// Returns true if the app is running in a desktop or web. /// Returns true if the app is running in a desktop or web.
bool get isDesktopDeviceOrWeb => false; bool get isDesktopDeviceOrWeb => isWeb || isDesktopDevice;
/// Returns true if the app is running in a flutter test environment. /// Returns true if the app is running in a flutter test environment.
bool get isTestEnvironment => CurrentPlatform.isFlutterTest; bool get isTestEnvironment => CurrentPlatform.isFlutterTest;
@@ -90,6 +90,7 @@ typedef ChannelInfoCallback = void Function(Channel);
/// {@template channelPreviewBuilder} /// {@template channelPreviewBuilder}
/// Builder used to create a custom [ChannelPreview] for a [Channel] /// Builder used to create a custom [ChannelPreview] for a [Channel]
/// {@endtemplate} /// {@endtemplate}
@Deprecated('Use StreamChannelListViewIndexedWidgetBuilder instead')
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
/// {@template viewInfoCallback} /// {@template viewInfoCallback}
@@ -348,6 +349,7 @@ typedef KeyEventPredicate = bool Function(FocusNode, KeyEvent);
/// {@template userItemBuilder} /// {@template userItemBuilder}
/// Builder used to create a custom [ListUserItem] from a [User] /// Builder used to create a custom [ListUserItem] from a [User]
/// {@endtemplate} /// {@endtemplate}
// ignore: avoid_positional_boolean_parameters
typedef UserItemBuilder = Widget Function(BuildContext, User, bool); typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
/// The action to perform when the "scroll to bottom" button is pressed /// The action to perform when the "scroll to bottom" button is pressed
+4 -4
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.1.0 version: 6.2.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
@@ -27,7 +27,7 @@ dependencies:
flutter_portal: ^1.0.0 flutter_portal: ^1.0.0
flutter_svg: ^2.0.4 flutter_svg: ^2.0.4
http_parser: ^4.0.0 http_parser: ^4.0.0
image_gallery_saver: ^1.7.1 image_gallery_saver: ^2.0.1
image_picker: ^0.8.2 image_picker: ^0.8.2
jiffy: ^5.0.0 jiffy: ^5.0.0
lottie: ^2.0.0 lottie: ^2.0.0
@@ -37,8 +37,8 @@ dependencies:
photo_view: ^0.14.0 photo_view: ^0.14.0
rxdart: ^0.27.0 rxdart: ^0.27.0
share_plus: ^6.3.0 share_plus: ^6.3.0
shimmer: ^2.0.0 shimmer: ^3.0.0
stream_chat_flutter_core: ^6.1.0 stream_chat_flutter_core: ^6.2.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
@@ -22,9 +22,8 @@ void main() {
EdgeInsets? padding, EdgeInsets? padding,
int initialScrollIndex = 0, int initialScrollIndex = 0,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -24,9 +24,8 @@ void main() {
double anchor = 0, double anchor = 0,
int itemCount = defaultItemCount, int itemCount = defaultItemCount,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -23,9 +23,8 @@ void main() {
double anchor = 0, double anchor = 0,
int itemCount = defaultItemCount, int itemCount = defaultItemCount,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -21,9 +21,8 @@ void main() {
EdgeInsets? padding, EdgeInsets? padding,
int initialIndex = 0, int initialIndex = 0,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -37,9 +37,8 @@ void main() {
double? minCacheExtent, double? minCacheExtent,
bool variableHeight = false, bool variableHeight = false,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -105,9 +104,8 @@ void main() {
testWidgets('List positioned with 0 at top - use default values', testWidgets('List positioned with 0 at top - use default values',
(WidgetTester tester) async { (WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -1543,9 +1541,8 @@ void main() {
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -1600,9 +1597,8 @@ void main() {
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -1661,9 +1657,8 @@ void main() {
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -1732,9 +1727,8 @@ void main() {
testWidgets('Jump to 100 then set itemCount to 0', testWidgets('Jump to 100 then set itemCount to 0',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
@@ -1781,9 +1775,8 @@ void main() {
testWidgets('List positioned with 100 at top then set itemCount to 100', testWidgets('List positioned with 100 at top then set itemCount to 100',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemCount = ValueNotifier<int>(defaultItemCount); final itemCount = ValueNotifier<int>(defaultItemCount);
@@ -1823,9 +1816,8 @@ void main() {
testWidgets('List positioned with 499 at bottom then set itemCount to 100', testWidgets('List positioned with 499 at bottom then set itemCount to 100',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemCount = ValueNotifier<int>(defaultItemCount); final itemCount = ValueNotifier<int>(defaultItemCount);
@@ -1943,9 +1935,8 @@ void main() {
}); });
testWidgets('Rebuild with scroll controller', (WidgetTester tester) async { testWidgets('Rebuild with scroll controller', (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final key = ValueNotifier<Key>(const ValueKey('key')); final key = ValueNotifier<Key>(const ValueKey('key'));
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
@@ -1986,9 +1977,8 @@ void main() {
testWidgets('Double rebuild with scroll controller', testWidgets('Double rebuild with scroll controller',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final outerKey = ValueNotifier<Key>(const ValueKey('outerKey')); final outerKey = ValueNotifier<Key>(const ValueKey('outerKey'));
final innerKey = GlobalKey(); final innerKey = GlobalKey();
final listKey = ValueNotifier<Key>(const ValueKey(null)); final listKey = ValueNotifier<Key>(const ValueKey(null));
@@ -2036,9 +2026,8 @@ void main() {
}); });
testWidgets('Key change with scroll controller', (WidgetTester tester) async { testWidgets('Key change with scroll controller', (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final key = ValueNotifier<Key>(const ValueKey('key')); final key = ValueNotifier<Key>(const ValueKey('key'));
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
@@ -2070,9 +2059,8 @@ void main() {
}); });
testWidgets('Scroll after rebuild', (WidgetTester tester) async { testWidgets('Scroll after rebuild', (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final key = ValueNotifier<Key>(const ValueKey('key')); final key = ValueNotifier<Key>(const ValueKey('key'));
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
@@ -2113,9 +2101,8 @@ void main() {
testWidgets('Scroll after rebuild when resusing state', testWidgets('Scroll after rebuild when resusing state',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final containerKey = ValueNotifier<Key>(const ValueKey('key')); final containerKey = ValueNotifier<Key>(const ValueKey('key'));
final scrollKey = GlobalKey(); final scrollKey = GlobalKey();
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
@@ -2158,9 +2145,8 @@ void main() {
testWidgets('Scroll after changing scroll controller', testWidgets('Scroll after changing scroll controller',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemScrollController0 = ItemScrollController(); final itemScrollController0 = ItemScrollController();
final itemScrollController1 = ItemScrollController(); final itemScrollController1 = ItemScrollController();
@@ -2205,9 +2191,8 @@ void main() {
testWidgets('Scroll after swapping scroll controllers', testWidgets('Scroll after swapping scroll controllers',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemScrollController0 = ItemScrollController(); final itemScrollController0 = ItemScrollController();
final itemScrollController1 = ItemScrollController(); final itemScrollController1 = ItemScrollController();
@@ -25,9 +25,8 @@ void main() {
double anchor = 0, double anchor = 0,
int itemCount = defaultItemCount, int itemCount = defaultItemCount,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -33,9 +33,8 @@ void main() {
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -100,9 +99,8 @@ void main() {
testWidgets('List positioned with 0 at top - use default values', testWidgets('List positioned with 0 at top - use default values',
(WidgetTester tester) async { (WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -484,9 +482,8 @@ void main() {
testWidgets('Empty list then update to single item list', testWidgets('Empty list then update to single item list',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
@@ -526,9 +523,8 @@ void main() {
testWidgets('ItemPositions: Empty list then update to 10 items list', testWidgets('ItemPositions: Empty list then update to 10 items list',
(WidgetTester tester) async { (WidgetTester tester) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create(); final itemPositionsListener = ItemPositionsListener.create();
@@ -24,9 +24,8 @@ void main() {
EdgeInsets? padding, EdgeInsets? padding,
int initialScrollIndex = 0, int initialScrollIndex = 0,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -25,9 +25,8 @@ void main() {
int itemCount = defaultItemCount, int itemCount = defaultItemCount,
bool reverse = false, bool reverse = false,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -421,9 +420,8 @@ void main() {
testWidgets('test nested positioned list', (WidgetTester tester) async { testWidgets('test nested positioned list', (WidgetTester tester) async {
const itemCount = 50; const itemCount = 50;
const key = Key('short_list'); const key = Key('short_list');
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -21,9 +21,8 @@ void main() {
EdgeInsets? padding, EdgeInsets? padding,
int initialIndex = 0, int initialIndex = 0,
}) async { }) async {
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.view.devicePixelRatio = 1.0;
tester.binding.window.physicalSizeTestValue = tester.view.physicalSize = const Size(screenWidth, screenHeight);
const Size(screenWidth, screenHeight);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@@ -39,20 +39,27 @@ void main() {
}); });
setUp(() { setUp(() {
methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
if (methodCall.method == 'listen') { .setMockMethodCallHandler(
try { methodChannel,
await ServicesBinding.instance.defaultBinaryMessenger (MethodCall methodCall) async {
.handlePlatformMessage( if (methodCall.method == 'listen') {
methodChannel.name, try {
methodChannel.codec.encodeSuccessEnvelope('wifi'), await TestDefaultBinaryMessengerBinding
(_) {}, .instance.defaultBinaryMessenger
); .handlePlatformMessage(
} catch (e) { methodChannel.name,
print(e); methodChannel.codec.encodeSuccessEnvelope('wifi'),
(_) {},
);
} catch (e) {
print(e);
}
} }
}
}); return null;
},
);
}); });
testWidgets( testWidgets(
@@ -118,7 +125,8 @@ void main() {
); );
tearDown(() { tearDown(() {
methodChannel.setMockMethodCallHandler(null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
}); });
/*testGoldens( /*testGoldens(
@@ -11,10 +11,13 @@ void main() {
const methodChannel = const methodChannel =
MethodChannel('dev.fluttercommunity.plus/connectivity_status'); MethodChannel('dev.fluttercommunity.plus/connectivity_status');
setUp(() { setUp(() {
methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel,
(MethodCall methodCall) async {
if (methodCall.method == 'listen') { if (methodCall.method == 'listen') {
try { try {
await ServicesBinding.instance.defaultBinaryMessenger await TestDefaultBinaryMessengerBinding
.instance.defaultBinaryMessenger
.handlePlatformMessage( .handlePlatformMessage(
methodChannel.name, methodChannel.name,
methodChannel.codec.encodeSuccessEnvelope('wifi'), methodChannel.codec.encodeSuccessEnvelope('wifi'),
@@ -24,6 +27,7 @@ void main() {
print(e); print(e);
} }
} }
return null;
}); });
}); });
@@ -87,7 +91,8 @@ void main() {
); );
tearDown(() { tearDown(() {
methodChannel.setMockMethodCallHandler(null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
}); });
}); });
} }
@@ -11,10 +11,13 @@ void main() {
const methodChannel = const methodChannel =
MethodChannel('dev.fluttercommunity.plus/connectivity_status'); MethodChannel('dev.fluttercommunity.plus/connectivity_status');
setUp(() { setUp(() {
methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel,
(MethodCall methodCall) async {
if (methodCall.method == 'listen') { if (methodCall.method == 'listen') {
try { try {
await ServicesBinding.instance.defaultBinaryMessenger await TestDefaultBinaryMessengerBinding
.instance.defaultBinaryMessenger
.handlePlatformMessage( .handlePlatformMessage(
methodChannel.name, methodChannel.name,
methodChannel.codec.encodeSuccessEnvelope('wifi'), methodChannel.codec.encodeSuccessEnvelope('wifi'),
@@ -24,6 +27,7 @@ void main() {
print(e); print(e);
} }
} }
return null;
}); });
}); });
@@ -93,7 +97,8 @@ void main() {
); );
tearDown(() { tearDown(() {
methodChannel.setMockMethodCallHandler(null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
}); });
}); });
} }
@@ -40,10 +40,12 @@ void main() {
}); });
setUp(() { setUp(() {
methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async {
if (methodCall.method == 'listen') { if (methodCall.method == 'listen') {
try { try {
await ServicesBinding.instance.defaultBinaryMessenger await TestDefaultBinaryMessengerBinding
.instance.defaultBinaryMessenger
.handlePlatformMessage( .handlePlatformMessage(
methodChannel.name, methodChannel.name,
methodChannel.codec.encodeSuccessEnvelope('wifi'), methodChannel.codec.encodeSuccessEnvelope('wifi'),
@@ -53,6 +55,7 @@ void main() {
print(e); print(e);
} }
} }
return null;
}); });
}); });
@@ -106,6 +109,7 @@ void main() {
}); });
tearDown(() { tearDown(() {
methodChannel.setMockMethodCallHandler(null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
}); });
} }
@@ -40,10 +40,12 @@ void main() {
}); });
setUp(() { setUp(() {
methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async {
if (methodCall.method == 'listen') { if (methodCall.method == 'listen') {
try { try {
await ServicesBinding.instance.defaultBinaryMessenger await TestDefaultBinaryMessengerBinding
.instance.defaultBinaryMessenger
.handlePlatformMessage( .handlePlatformMessage(
methodChannel.name, methodChannel.name,
methodChannel.codec.encodeSuccessEnvelope('wifi'), methodChannel.codec.encodeSuccessEnvelope('wifi'),
@@ -53,6 +55,7 @@ void main() {
print(e); print(e);
} }
} }
return null;
}); });
}); });
@@ -110,6 +113,7 @@ void main() {
}); });
tearDown(() { tearDown(() {
methodChannel.setMockMethodCallHandler(null); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
}); });
} }
@@ -69,11 +69,11 @@ final _messageInputThemeControlMidLerp = StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300), sendAnimationDuration: const Duration(milliseconds: 300),
inputBackgroundColor: const Color(0xff87898b), inputBackgroundColor: const Color(0xff87898b),
actionButtonColor: const Color(0xff005fff), actionButtonColor: const Color(0xff196eff),
actionButtonIdleColor: const Color(0xff7a7a7a), actionButtonIdleColor: const Color(0xff7a7a7a),
sendButtonColor: const Color(0xff005fff), sendButtonColor: const Color(0xff196eff),
sendButtonIdleColor: const Color(0xff848585), sendButtonIdleColor: const Color(0xff848585),
expandButtonColor: const Color(0xff005fff), expandButtonColor: const Color(0xff196eff),
inputTextStyle: const TextStyle( inputTextStyle: const TextStyle(
color: Color(0xff7f7f7f), color: Color(0xff7f7f7f),
fontSize: 14, fontSize: 14,
@@ -1,9 +1,18 @@
## 6.2.0
- Fixed `StreamMessageInputController.textPatternStyle` not matching case-insensitive patterns.
- Updated `connectivity_plus` dependency to `^4.0.0`
- Fixed `StreamChannel` shows black screen while loading in some cases.
- Updated `stream_chat` dependency to [`6.2.0`](https://pub.dev/packages/stream_chat/changelog).
## 6.1.0 ## 6.1.0
- Updated `dart` sdk environment range to support `3.0.0`. - Updated `dart` sdk environment range to support `3.0.0`.
- Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog).
- [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being - [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being
hidden. hidden.
- [[#1540]](https://github.com/GetStream/stream-chat-flutter/issues/1540) Use `CircularProgressIndicator.adaptive`
instead of material indicator.
## 6.0.0 ## 6.0.0
@@ -41,7 +41,7 @@ bool Function(Message) defaultMessageFilter(String currentUserId) =>
/// }, /// },
/// loadingBuilder: (context) { /// loadingBuilder: (context) {
/// return Center( /// return Center(
/// child: CircularProgressIndicator(), /// child: CircularProgressIndicator.adaptive(),
/// ); /// );
/// }, /// },
/// messageListBuilder: (context, list) { /// messageListBuilder: (context, list) {
@@ -41,7 +41,10 @@ class MessageTextFieldController extends TextEditingController {
} }
return TextSpan(text: text, style: style).splitMapJoin( return TextSpan(text: text, style: style).splitMapJoin(
RegExp(pattern.keys.map((it) => it.pattern).join('|')), RegExp(
pattern.keys.map((it) => it.pattern).join('|'),
caseSensitive: false,
),
onMatch: (match) { onMatch: (match) {
final text = match[0]!; final text = match[0]!;
final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); final key = pattern.keys.firstWhere((it) => it.hasMatch(text));
@@ -251,14 +251,14 @@ class StreamChannelState extends State<StreamChannel> {
channel.state!.truncate(); channel.state!.truncate();
if (messageId == null) { if (messageId == null) {
await channel.query( final state = await channel.query(
messagesPagination: PaginationParams( messagesPagination: PaginationParams(
limit: limit, limit: limit,
), ),
preferOffline: preferOffline, preferOffline: preferOffline,
); );
channel.state!.isUpToDate = true; channel.state!.isUpToDate = true;
return null; return state;
} }
return channel.query( return channel.query(
@@ -445,13 +445,13 @@ class StreamChannelState extends State<StreamChannel> {
final dataLoaded = snapshot.data?.every((it) => it) == true; final dataLoaded = snapshot.data?.every((it) => it) == true;
if (widget.showLoading && !dataLoaded) { if (widget.showLoading && !dataLoaded) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
); );
} }
return widget.child; return widget.child;
}, },
); );
if (initialMessageId != null) { if (_futures.length > 1) {
child = Material(child: child); child = Material(child: child);
} }
return child; return child;
@@ -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.1.0 version: 6.2.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
@@ -11,13 +11,13 @@ environment:
dependencies: dependencies:
collection: ^1.17.0 collection: ^1.17.0
connectivity_plus: ^3.0.2 connectivity_plus: ^4.0.0
flutter: flutter:
sdk: flutter sdk: flutter
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.1.0 stream_chat: ^6.2.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
@@ -1,3 +1,7 @@
## 5.2.0
* Updated `stream_chat_flutter` dependency to [`6.2.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
## 5.1.0 ## 5.1.0
* Updated `dart` sdk environment range to support `3.0.0`. * Updated `dart` sdk environment range to support `3.0.0`.
@@ -577,10 +577,10 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -106,10 +106,10 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -133,10 +133,10 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
appBar: const StreamChannelHeader(), appBar: StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView(), child: StreamMessageListView(),
), ),
@@ -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.1.0 version: 5.2.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.1.0 stream_chat_flutter: ^6.2.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^5.7.2 dart_code_metrics: ^5.7.2
@@ -1,3 +1,10 @@
## 6.2.0
- Added support for `StreamChatPersistenceClient.isConnected` for checking if the client is connected to the database.
- [[#1422]](https://github.com/GetStream/stream-chat-flutter/issues/1422) Removed default values
from `UserEntity` `createdAt` and `updatedAt` fields.
- Updated `stream_chat` dependency to [`6.2.0`](https://pub.dev/packages/stream_chat/changelog).
## 6.1.0 ## 6.1.0
- Updated `dart` sdk environment range to support `3.0.0`. - Updated `dart` sdk environment range to support `3.0.0`.
@@ -81,13 +81,13 @@ class ChannelQueryDao extends DatabaseAccessor<DriftChatDatabase>
final cachedChannelCids = await getCachedChannelCids(filter); final cachedChannelCids = await getCachedChannelCids(filter);
final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids)); final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids));
final cachedChannels = await (query.join([ final cachedChannels = await query.join([
leftOuterJoin(users, channels.createdById.equalsExp(users.id)), leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
]).map((row) { ]).map((row) {
final createdByEntity = row.readTableOrNull(users); final createdByEntity = row.readTableOrNull(users);
final channelEntity = row.readTable(channels); final channelEntity = row.readTable(channels);
return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser()); return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser());
})).get(); }).get();
var chainedComparator = (ChannelModel a, ChannelModel b) { var chainedComparator = (ChannelModel a, ChannelModel b) {
final dateA = a.lastMessageAt ?? a.createdAt; final dateA = a.lastMessageAt ?? a.createdAt;
@@ -1,10 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart'; 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/members.dart'; import 'package:stream_chat_persistence/src/entity/members.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 'member_dao.g.dart'; part 'member_dao.g.dart';
@@ -39,9 +37,9 @@ class MemberDao extends DatabaseAccessor<DriftChatDatabase>
) { ) {
final entities = channelWithMembers.entries final entities = channelWithMembers.entries
.map((entry) => .map((entry) =>
(entry.value?.map( entry.value?.map(
(member) => member.toEntity(cid: entry.key), (member) => member.toEntity(cid: entry.key),
)) ?? ) ??
[]) [])
.expand((it) => it) .expand((it) => it)
.toList(growable: false); .toList(growable: false);
@@ -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 => 10; int get schemaVersion => 11;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -3651,18 +3651,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> {
const VerificationMeta('createdAt'); const VerificationMeta('createdAt');
@override @override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false, 'created_at', aliasedName, true,
type: DriftSqlType.dateTime, type: DriftSqlType.dateTime, requiredDuringInsert: false);
requiredDuringInsert: false,
defaultValue: currentDateAndTime);
static const VerificationMeta _updatedAtMeta = static const VerificationMeta _updatedAtMeta =
const VerificationMeta('updatedAt'); const VerificationMeta('updatedAt');
@override @override
late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>(
'updated_at', aliasedName, false, 'updated_at', aliasedName, true,
type: DriftSqlType.dateTime, type: DriftSqlType.dateTime, requiredDuringInsert: false);
requiredDuringInsert: false,
defaultValue: currentDateAndTime);
static const VerificationMeta _lastActiveMeta = static const VerificationMeta _lastActiveMeta =
const VerificationMeta('lastActive'); const VerificationMeta('lastActive');
@override @override
@@ -3773,9 +3769,9 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> {
language: attachedDatabase.typeMapping language: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}language']), .read(DriftSqlType.string, data['${effectivePrefix}language']),
createdAt: attachedDatabase.typeMapping createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']),
updatedAt: attachedDatabase.typeMapping updatedAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at']),
lastActive: attachedDatabase.typeMapping lastActive: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}last_active']), .read(DriftSqlType.dateTime, data['${effectivePrefix}last_active']),
online: attachedDatabase.typeMapping online: attachedDatabase.typeMapping
@@ -3808,10 +3804,10 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
final String? language; final String? language;
/// Date of user creation /// Date of user creation
final DateTime createdAt; final DateTime? createdAt;
/// Date of last user update /// Date of last user update
final DateTime updatedAt; final DateTime? updatedAt;
/// Date of last user connection /// Date of last user connection
final DateTime? lastActive; final DateTime? lastActive;
@@ -3828,8 +3824,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
{required this.id, {required this.id,
this.role, this.role,
this.language, this.language,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
this.lastActive, this.lastActive,
required this.online, required this.online,
required this.banned, required this.banned,
@@ -3844,8 +3840,12 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
if (!nullToAbsent || language != null) { if (!nullToAbsent || language != null) {
map['language'] = Variable<String>(language); map['language'] = Variable<String>(language);
} }
map['created_at'] = Variable<DateTime>(createdAt); if (!nullToAbsent || createdAt != null) {
map['updated_at'] = Variable<DateTime>(updatedAt); map['created_at'] = Variable<DateTime>(createdAt);
}
if (!nullToAbsent || updatedAt != null) {
map['updated_at'] = Variable<DateTime>(updatedAt);
}
if (!nullToAbsent || lastActive != null) { if (!nullToAbsent || lastActive != null) {
map['last_active'] = Variable<DateTime>(lastActive); map['last_active'] = Variable<DateTime>(lastActive);
} }
@@ -3865,8 +3865,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
role: serializer.fromJson<String?>(json['role']), role: serializer.fromJson<String?>(json['role']),
language: serializer.fromJson<String?>(json['language']), language: serializer.fromJson<String?>(json['language']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime?>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), updatedAt: serializer.fromJson<DateTime?>(json['updatedAt']),
lastActive: serializer.fromJson<DateTime?>(json['lastActive']), lastActive: serializer.fromJson<DateTime?>(json['lastActive']),
online: serializer.fromJson<bool>(json['online']), online: serializer.fromJson<bool>(json['online']),
banned: serializer.fromJson<bool>(json['banned']), banned: serializer.fromJson<bool>(json['banned']),
@@ -3880,8 +3880,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'role': serializer.toJson<String?>(role), 'role': serializer.toJson<String?>(role),
'language': serializer.toJson<String?>(language), 'language': serializer.toJson<String?>(language),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime?>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt), 'updatedAt': serializer.toJson<DateTime?>(updatedAt),
'lastActive': serializer.toJson<DateTime?>(lastActive), 'lastActive': serializer.toJson<DateTime?>(lastActive),
'online': serializer.toJson<bool>(online), 'online': serializer.toJson<bool>(online),
'banned': serializer.toJson<bool>(banned), 'banned': serializer.toJson<bool>(banned),
@@ -3893,8 +3893,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
{String? id, {String? id,
Value<String?> role = const Value.absent(), Value<String?> role = const Value.absent(),
Value<String?> language = const Value.absent(), Value<String?> language = const Value.absent(),
DateTime? createdAt, Value<DateTime?> createdAt = const Value.absent(),
DateTime? updatedAt, Value<DateTime?> updatedAt = const Value.absent(),
Value<DateTime?> lastActive = const Value.absent(), Value<DateTime?> lastActive = const Value.absent(),
bool? online, bool? online,
bool? banned, bool? banned,
@@ -3903,8 +3903,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
id: id ?? this.id, id: id ?? this.id,
role: role.present ? role.value : this.role, role: role.present ? role.value : this.role,
language: language.present ? language.value : this.language, language: language.present ? language.value : this.language,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt.present ? createdAt.value : this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt.present ? updatedAt.value : this.updatedAt,
lastActive: lastActive.present ? lastActive.value : this.lastActive, lastActive: lastActive.present ? lastActive.value : this.lastActive,
online: online ?? this.online, online: online ?? this.online,
banned: banned ?? this.banned, banned: banned ?? this.banned,
@@ -3948,8 +3948,8 @@ class UsersCompanion extends UpdateCompanion<UserEntity> {
final Value<String> id; final Value<String> id;
final Value<String?> role; final Value<String?> role;
final Value<String?> language; final Value<String?> language;
final Value<DateTime> createdAt; final Value<DateTime?> createdAt;
final Value<DateTime> updatedAt; final Value<DateTime?> updatedAt;
final Value<DateTime?> lastActive; final Value<DateTime?> lastActive;
final Value<bool> online; final Value<bool> online;
final Value<bool> banned; final Value<bool> banned;
@@ -4010,8 +4010,8 @@ class UsersCompanion extends UpdateCompanion<UserEntity> {
{Value<String>? id, {Value<String>? id,
Value<String?>? role, Value<String?>? role,
Value<String?>? language, Value<String?>? language,
Value<DateTime>? createdAt, Value<DateTime?>? createdAt,
Value<DateTime>? updatedAt, Value<DateTime?>? updatedAt,
Value<DateTime?>? lastActive, Value<DateTime?>? lastActive,
Value<bool>? online, Value<bool>? online,
Value<bool>? banned, Value<bool>? banned,
@@ -15,10 +15,10 @@ class Users extends Table {
TextColumn get language => text().nullable()(); TextColumn get language => text().nullable()();
/// Date of user creation /// Date of user creation
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().nullable()();
/// Date of last user update /// Date of last user update
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get updatedAt => dateTime().nullable()();
/// Date of last user connection /// Date of last user connection
DateTimeColumn get lastActive => dateTime().nullable()(); DateTimeColumn get lastActive => dateTime().nullable()();
@@ -58,7 +58,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
bool get _debugIsConnected { bool get _debugIsConnected {
assert(() { assert(() {
if (db == null) { if (!isConnected) {
throw StateError(''' throw StateError('''
$runtimeType hasn't been connected yet or used after `disconnect` $runtimeType hasn't been connected yet or used after `disconnect`
was called. Consider calling `connect` to create a connection. was called. Consider calling `connect` to create a connection.
@@ -79,6 +79,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported, webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported,
); );
@override
bool get isConnected => db != null;
@override @override
Future<void> connect( Future<void> connect(
String userId, { String userId, {
@@ -405,7 +408,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
Future<void> disconnect({bool flush = false}) async { Future<void> disconnect({bool flush = false}) async {
_logger.info('disconnect'); _logger.info('disconnect');
if (db != null) { if (isConnected) {
_logger.info('Disconnecting'); _logger.info('Disconnecting');
if (flush) { if (flush) {
_logger.info('Flushing'); _logger.info('Flushing');
@@ -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.1.0 version: 6.2.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.1.0 stream_chat: ^6.2.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
@@ -132,7 +132,7 @@ void main() {
// Should match lastMessageAt date // Should match lastMessageAt date
expect( expect(
updatedChannel.lastMessageAt, updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt!), isSameDateAs(insertedChannel.lastMessageAt),
); );
} }
}); });
@@ -177,7 +177,7 @@ void main() {
// Should match lastMessageAt date // Should match lastMessageAt date
expect( expect(
updatedChannel.lastMessageAt, updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt!), isSameDateAs(insertedChannel.lastMessageAt),
); );
} }
}); });
@@ -220,7 +220,7 @@ void main() {
// Should match lastMessageAt date // Should match lastMessageAt date
expect( expect(
updatedChannel.lastMessageAt, updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt!), isSameDateAs(insertedChannel.lastMessageAt),
); );
} }
}); });
@@ -66,7 +66,7 @@ void main() {
expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt)); expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt));
expect( expect(
fetchedMember.inviteAcceptedAt, fetchedMember.inviteAcceptedAt,
isSameDateAs(member.inviteAcceptedAt!), isSameDateAs(member.inviteAcceptedAt),
); );
} }
}); });
@@ -93,7 +93,7 @@ void main() {
expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt)); expect(fetchedMember.updatedAt, isSameDateAs(member.updatedAt));
expect( expect(
fetchedMember.inviteAcceptedAt, fetchedMember.inviteAcceptedAt,
isSameDateAs(member.inviteAcceptedAt!), isSameDateAs(member.inviteAcceptedAt),
); );
} }
@@ -37,8 +37,8 @@ void main() {
expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt)); expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt));
expect(channelModel.memberCount, entity.memberCount); expect(channelModel.memberCount, entity.memberCount);
expect(channelModel.cid, entity.cid); expect(channelModel.cid, entity.cid);
expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt!)); expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt));
expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt!)); expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt));
expect(channelModel.extraData, entity.extraData); expect(channelModel.extraData, entity.extraData);
expect(channelModel.createdBy!.id, entity.createdById); expect(channelModel.createdBy!.id, entity.createdById);
}); });
@@ -77,8 +77,8 @@ void main() {
expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt)); expect(channelModel.updatedAt, isSameDateAs(entity.updatedAt));
expect(channelModel.memberCount, entity.memberCount); expect(channelModel.memberCount, entity.memberCount);
expect(channelModel.cid, entity.cid); expect(channelModel.cid, entity.cid);
expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt!)); expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt));
expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt!)); expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt));
expect(channelModel.extraData, entity.extraData); expect(channelModel.extraData, entity.extraData);
expect(channelModel.createdBy!.id, entity.createdById); expect(channelModel.createdBy!.id, entity.createdById);
}); });
@@ -115,8 +115,8 @@ void main() {
expect(channelEntity.updatedAt, isSameDateAs(model.updatedAt)); expect(channelEntity.updatedAt, isSameDateAs(model.updatedAt));
expect(channelEntity.memberCount, model.memberCount); expect(channelEntity.memberCount, model.memberCount);
expect(channelEntity.cid, model.cid); expect(channelEntity.cid, model.cid);
expect(channelEntity.lastMessageAt, isSameDateAs(model.lastMessageAt!)); expect(channelEntity.lastMessageAt, isSameDateAs(model.lastMessageAt));
expect(channelEntity.deletedAt, isSameDateAs(model.deletedAt!)); expect(channelEntity.deletedAt, isSameDateAs(model.deletedAt));
expect(channelEntity.extraData, model.extraData); expect(channelEntity.extraData, model.extraData);
expect(channelEntity.createdById, model.createdBy!.id); expect(channelEntity.createdById, model.createdBy!.id);
}); });
@@ -29,8 +29,8 @@ void main() {
expect(member.createdAt, isSameDateAs(entity.createdAt)); expect(member.createdAt, isSameDateAs(entity.createdAt));
expect(member.updatedAt, isSameDateAs(entity.updatedAt)); expect(member.updatedAt, isSameDateAs(entity.updatedAt));
expect(member.channelRole, entity.channelRole); expect(member.channelRole, entity.channelRole);
expect(member.inviteAcceptedAt, isSameDateAs(entity.inviteAcceptedAt!)); expect(member.inviteAcceptedAt, isSameDateAs(entity.inviteAcceptedAt));
expect(member.inviteRejectedAt, isSameDateAs(entity.inviteRejectedAt!)); expect(member.inviteRejectedAt, isSameDateAs(entity.inviteRejectedAt));
expect(member.invited, entity.invited); expect(member.invited, entity.invited);
expect(member.banned, entity.banned); expect(member.banned, entity.banned);
expect(member.shadowBanned, entity.shadowBanned); expect(member.shadowBanned, entity.shadowBanned);
@@ -59,8 +59,8 @@ void main() {
expect(entity.createdAt, isSameDateAs(member.createdAt)); expect(entity.createdAt, isSameDateAs(member.createdAt));
expect(entity.updatedAt, isSameDateAs(member.updatedAt)); expect(entity.updatedAt, isSameDateAs(member.updatedAt));
expect(entity.channelRole, member.channelRole); expect(entity.channelRole, member.channelRole);
expect(entity.inviteAcceptedAt, isSameDateAs(member.inviteAcceptedAt!)); expect(entity.inviteAcceptedAt, isSameDateAs(member.inviteAcceptedAt));
expect(entity.inviteRejectedAt, isSameDateAs(member.inviteRejectedAt!)); expect(entity.inviteRejectedAt, isSameDateAs(member.inviteRejectedAt));
expect(entity.invited, member.invited); expect(entity.invited, member.invited);
expect(entity.banned, member.banned); expect(entity.banned, member.banned);
expect(entity.shadowBanned, member.shadowBanned); expect(entity.shadowBanned, member.shadowBanned);
@@ -96,11 +96,11 @@ void main() {
expect(message.updatedAt, isSameDateAs(entity.updatedAt)); expect(message.updatedAt, isSameDateAs(entity.updatedAt));
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.deletedAt, isSameDateAs(entity.deletedAt));
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));
expect(message.pinnedAt, isSameDateAs(entity.pinnedAt!)); expect(message.pinnedAt, isSameDateAs(entity.pinnedAt));
expect(message.pinnedBy!.id, entity.pinnedByUserId); expect(message.pinnedBy!.id, entity.pinnedByUserId);
expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionCounts, entity.reactionCounts);
expect(message.reactionScores, entity.reactionScores); expect(message.reactionScores, entity.reactionScores);
@@ -191,11 +191,11 @@ void main() {
expect(entity.updatedAt, isSameDateAs(message.updatedAt)); expect(entity.updatedAt, isSameDateAs(message.updatedAt));
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.deletedAt, isSameDateAs(message.deletedAt));
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));
expect(entity.pinnedAt, isSameDateAs(message.pinnedAt!)); expect(entity.pinnedAt, isSameDateAs(message.pinnedAt));
expect(entity.pinnedByUserId, message.pinnedBy!.id); expect(entity.pinnedByUserId, message.pinnedBy!.id);
expect(entity.reactionCounts, message.reactionCounts); expect(entity.reactionCounts, message.reactionCounts);
expect(entity.reactionScores, message.reactionScores); expect(entity.reactionScores, message.reactionScores);
@@ -89,11 +89,11 @@ void main() {
expect(message.updatedAt, isSameDateAs(entity.updatedAt)); expect(message.updatedAt, isSameDateAs(entity.updatedAt));
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.deletedAt, isSameDateAs(entity.deletedAt));
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));
expect(message.pinnedAt, isSameDateAs(entity.pinnedAt!)); expect(message.pinnedAt, isSameDateAs(entity.pinnedAt));
expect(message.pinnedBy!.id, entity.pinnedByUserId); expect(message.pinnedBy!.id, entity.pinnedByUserId);
expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionCounts, entity.reactionCounts);
expect(message.reactionScores, entity.reactionScores); expect(message.reactionScores, entity.reactionScores);
@@ -179,11 +179,11 @@ void main() {
expect(entity.updatedAt, isSameDateAs(message.updatedAt)); expect(entity.updatedAt, isSameDateAs(message.updatedAt));
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.deletedAt, isSameDateAs(message.deletedAt));
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));
expect(entity.pinnedAt, isSameDateAs(message.pinnedAt!)); expect(entity.pinnedAt, isSameDateAs(message.pinnedAt));
expect(entity.pinnedByUserId, message.pinnedBy!.id); expect(entity.pinnedByUserId, message.pinnedBy!.id);
expect(entity.reactionCounts, message.reactionCounts); expect(entity.reactionCounts, message.reactionCounts);
expect(entity.reactionScores, message.reactionScores); expect(entity.reactionScores, message.reactionScores);
@@ -27,7 +27,7 @@ void main() {
expect(user.language, entity.language); expect(user.language, entity.language);
expect(user.createdAt, isSameDateAs(entity.createdAt)); expect(user.createdAt, isSameDateAs(entity.createdAt));
expect(user.updatedAt, isSameDateAs(entity.updatedAt)); expect(user.updatedAt, isSameDateAs(entity.updatedAt));
expect(user.lastActive, isSameDateAs(entity.lastActive!)); expect(user.lastActive, isSameDateAs(entity.lastActive));
expect(user.online, entity.online); expect(user.online, entity.online);
expect(user.banned, entity.banned); expect(user.banned, entity.banned);
expect(user.extraData, entity.extraData); expect(user.extraData, entity.extraData);
@@ -52,7 +52,7 @@ void main() {
expect(entity.language, user.language); expect(entity.language, user.language);
expect(entity.createdAt, isSameDateAs(user.createdAt)); expect(entity.createdAt, isSameDateAs(user.createdAt));
expect(entity.updatedAt, isSameDateAs(user.updatedAt)); expect(entity.updatedAt, isSameDateAs(user.updatedAt));
expect(entity.lastActive, isSameDateAs(user.lastActive!)); expect(entity.lastActive, isSameDateAs(user.lastActive));
expect(entity.online, user.online); expect(entity.online, user.online);
expect(entity.banned, user.banned); expect(entity.banned, user.banned);
expect(entity.extraData, user.extraData); expect(entity.extraData, user.extraData);
@@ -1,21 +1,22 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
Matcher isSameDateAs(DateTime targetDate) => Matcher isSameDateAs(DateTime? targetDate) =>
_IsSameDateAs(targetDate: targetDate); _IsSameDateAs(targetDate: targetDate);
class _IsSameDateAs extends Matcher { class _IsSameDateAs extends Matcher {
const _IsSameDateAs({required this.targetDate}); const _IsSameDateAs({required this.targetDate});
final DateTime targetDate; final DateTime? targetDate;
@override @override
bool matches(covariant DateTime date, Map matchState) => bool matches(covariant DateTime date, Map matchState) {
date.year == targetDate.year && return date.year == targetDate?.year &&
date.month == targetDate.month && date.month == targetDate?.month &&
date.day == targetDate.day && date.day == targetDate?.day &&
date.hour == targetDate.hour && date.hour == targetDate?.hour &&
date.minute == targetDate.minute && date.minute == targetDate?.minute &&
date.second == targetDate.second; date.second == targetDate?.second;
}
@override @override
Description describe(Description description) => Description describe(Description description) =>
@@ -16,9 +16,9 @@ void main() {
const userId = 'testUserId'; const userId = 'testUserId';
test('successfully connects with the Database', () async { test('successfully connects with the Database', () async {
final client = StreamChatPersistenceClient(logLevel: Level.ALL); final client = StreamChatPersistenceClient(logLevel: Level.ALL);
expect(client.db, isNull); expect(client.isConnected, false);
await client.connect(userId, databaseProvider: testDatabaseProvider); await client.connect(userId, databaseProvider: testDatabaseProvider);
expect(client.db, isNotNull); expect(client.isConnected, true);
expect(client.db, isA<DriftChatDatabase>()); expect(client.db, isA<DriftChatDatabase>());
expect(client.db!.userId, userId); expect(client.db!.userId, userId);
@@ -29,10 +29,9 @@ void main() {
test('throws if already connected', () async { test('throws if already connected', () async {
final client = StreamChatPersistenceClient(logLevel: Level.ALL); final client = StreamChatPersistenceClient(logLevel: Level.ALL);
expect(client.db, isNull); expect(client.isConnected, false);
await client.connect(userId, databaseProvider: testDatabaseProvider); await client.connect(userId, databaseProvider: testDatabaseProvider);
expect(client.db, isNotNull); expect(client.isConnected, true);
expect(client.db, isNotNull);
expect(client.db, isA<DriftChatDatabase>()); expect(client.db, isA<DriftChatDatabase>());
expect(client.db!.userId, userId); expect(client.db!.userId, userId);
expect( expect(
@@ -50,9 +49,9 @@ void main() {
const userId = 'testUserId'; const userId = 'testUserId';
final client = StreamChatPersistenceClient(logLevel: Level.ALL); final client = StreamChatPersistenceClient(logLevel: Level.ALL);
await client.connect(userId, databaseProvider: testDatabaseProvider); await client.connect(userId, databaseProvider: testDatabaseProvider);
expect(client.db, isNotNull); expect(client.isConnected, true);
await client.disconnect(flush: true); await client.disconnect(flush: true);
expect(client.db, isNull); expect(client.isConnected, false);
}); });
test('client function throws stateError if db is not yet connected', () { test('client function throws stateError if db is not yet connected', () {