Merge branch 'develop' into feat/messageInput

This commit is contained in:
Sahil Kumar
2023-04-07 03:27:37 +05:30
committed by GitHub
303 changed files with 3808 additions and 1842 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ pubspec.lock
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
# Avoid committing generated JavaScript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
+45 -13
View File
@@ -1,3 +1,35 @@
## Upcoming
🐞 Fixed
- Fixed streamWatchers. Before it was always new, now it is possible to follow the watchers of a channel.
- Make `Message.i18n` field read-only.
🔄 Changed
- Cancelling a attachment upload now removes the attachment from the message.
- Updated `dio` and other dependencies to resolvable versions.
✅ Added
- Added `presence` property to `Channel::watch` method.
## 5.3.0
🔄 Changed
- Updated `rate_limiter` dependency to `^1.0.0`
## 5.2.0
✅ Added
- Added `Huawei` and `Xiaomi` PushProviders.
🐞 Fixed
- Fixed initializing last synced date.
## 5.1.0
✅ Added
@@ -17,7 +49,7 @@
🛑️ Breaking Changes from `5.0.0-beta.2`
- `Channel.addMembers`, `Channel.removeMembers`, `Channel.inviteMembers` and `Channel.update`
positional params are now optional params.
positional parameters are now optional parameters.
```dart
// previous
@@ -390,21 +422,21 @@ the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migra
🛑️ Breaking Changes from `1.5.3`
- migrate this package to null safety
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the
- `ConnectUserWithProvider` now requires `tokenProvider` as a required parameter. (Removed from the
constructor)
- `client.disconnect()` is now divided into two different functions
- `client.closeConnection()` -> for closing user websocket connection.
- `client.closeConnection()` -> for closing user web socket connection.
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
- `client.devToken()` now returns a `Token` model instead of `String`.
- `ApiError` is removed in favor of `StreamChatError`
- `StreamChatError` -> parent type for all the stream errors.
- `StreamWebSocketError` -> for user websocket related errors.
- `StreamWebSocketError` -> for user web socket related errors.
- `StreamChatNetworkError` -> for network related errors.
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
- `client.queryChannels()`, `channel.query()` options parameter is removed in favor of individual parameters
- `option.state` -> bool state
- `option.watch` -> bool watch
- `option.presence` -> bool presence
- `client.queryUsers()` options param is removed in favor of individual params
- `client.queryUsers()` options parameter is removed in favor of individual parameters
- `option.presence` -> bool presence
- Migrate this package to null safety
- Added typed filters
@@ -438,28 +470,28 @@ the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migra
🛑️ Breaking Changes from `2.0.0-nullsafety.6`
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the
- `ConnectUserWithProvider` now requires `tokenProvider` as a required parameter. (Removed from the
constructor)
- `client.disconnect()` is now divided into two different functions
- `client.closeConnection()` -> for closing user websocket connection.
- `client.closeConnection()` -> for closing user web socket connection.
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
- `client.devToken()` now returns a `Token` model instead of `String`.
- `ApiError` is removed in favor of `StreamChatError`
- `StreamChatError` -> parent type for all the stream errors.
- `StreamWebSocketError` -> for user websocket related errors.
- `StreamWebSocketError` -> for user web socket related errors.
- `StreamChatNetworkError` -> for network related errors.
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
- `client.queryChannels()`, `channel.query()` options parameter is removed in favor of individual parameters
- `option.state` -> bool state
- `option.watch` -> bool watch
- `option.presence` -> bool presence
- `client.queryUsers()` options param is removed in favor of individual params
- `client.queryUsers()` options parameter is removed in favor of individual parameters
- `option.presence` -> bool presence
✅ Added
- New `Location` enum is introduced for easily changing the client location/baseUrl.
- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect
user ws connection.
user WS connection.
🔄 Changed
@@ -562,7 +594,7 @@ the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migra
- 🛑 **BREAKING** Renamed `Client` to less generic `StreamChatClient`
- 🛑 **BREAKING** Segregated the persistence layer into separate
package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence)
package [`stream_chat_persistence`](https://pub.dev/packages/stream_chat_persistence)
- 🛑 **BREAKING** Moved `Client.backgroundKeepAlive`
to [core package](https://pub.dev/packages/stream_chat_core)
- 🛑 **BREAKING** Moved `Client.showLocalNotification`
+6 -6
View File
@@ -39,7 +39,7 @@ There is a detailed Flutter example project in the `example` folder. You can dir
## Setup API Client
First you need to instantiate a chat client. The Chat client will manage API call, event handling and manage the websocket connection to Stream Chat servers. You should only create the client once and re-use it across your application.
First you need to instantiate a chat client. The Chat client will manage API call, event handling and manage the web socket connection to Stream Chat servers. You should only create the client once and re-use it across your application.
```dart
final client = StreamChatClient("stream-chat-api-key");
@@ -47,7 +47,7 @@ final client = StreamChatClient("stream-chat-api-key");
### Logging
By default the Chat Client will write all messages with level Warn or Error to stdout.
By default the Chat Client will write all messages with level Warn or Error to `stdout`.
#### Change Logging Level
@@ -59,7 +59,7 @@ final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
#### Custom Logger
You can handle the log messages directly instead of have them written to stdout, this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility.
You can handle the log messages directly instead of have them written to `stdout`, this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility.
```dart
myLogHandlerFunction = (LogRecord record) {
@@ -71,7 +71,7 @@ final client = StreamChatClient("stream-chat-api-key", logHandlerFunction: myLog
### Offline storage
To add data persistance you can extend the class `ChatPersistenceClient` and pass an instance to the `StreamChatClient`.
To add data persistence, you can extend the class `ChatPersistenceClient` and pass an instance to the `StreamChatClient`.
```dart
class CustomChatPersistentClient extends ChatPersistenceClient {
@@ -84,7 +84,7 @@ final client = StreamChatClient(
)..chatPersistenceClient = CustomChatPersistentClient();
```
We provide an official persistent client in the [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) package.
We provide an official persistent client in the [`stream_chat_persistence`](https://pub.dev/packages/stream_chat_persistence) package.
```dart
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
@@ -104,7 +104,7 @@ final client = StreamChatClient(
### Code conventions
- Make sure that you run `dartfmt` before commiting your code
- Make sure that you run `dartfmt` before you commit your code
- Make sure all public methods and functions are well documented
### Running tests
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 29
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -40,7 +40,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -6,15 +6,16 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="example"
android:icon="@mipmap/ic_launcher">
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.7.21'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
@@ -6,6 +6,7 @@ version: 1.0.0+1
environment:
sdk: '>=2.17.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
cupertino_icons: ^1.0.0
@@ -463,12 +463,19 @@ class Channel {
client.logger.info('Found ${attachments.length} attachments');
void updateAttachment(Attachment attachment) {
void updateAttachment(Attachment attachment, {bool remove = false}) {
final index = message!.attachments.indexWhere(
(it) => it.id == attachment.id,
);
if (index != -1) {
final newAttachments = [...message!.attachments]..[index] = attachment;
// update or remove attachment from message.
final List<Attachment> newAttachments;
if (remove) {
newAttachments = [...message!.attachments]..removeAt(index);
} else {
newAttachments = [...message!.attachments]..[index] = attachment;
}
final updatedMessage = message!.copyWith(attachments: newAttachments);
state?.updateMessage(updatedMessage);
// updating original message for next iteration
@@ -533,6 +540,14 @@ class Channel {
);
}
}).catchError((e, stk) {
if (e is StreamChatNetworkError && e.isRequestCancelledError) {
client.logger.info('Attachment ${it.id} upload cancelled');
// remove attachment from message if cancelled.
updateAttachment(it, remove: true);
return;
}
client.logger.severe('error uploading the attachment', e, stk);
updateAttachment(
it.copyWith(uploadState: UploadState.failed(error: e.toString())),
@@ -1219,11 +1234,11 @@ class Channel {
}
/// Loads the initial channel state and watches for changes.
Future<ChannelState> watch() async {
Future<ChannelState> watch({bool presence = false}) async {
ChannelState response;
try {
response = await query(watch: true);
response = await query(watch: true, presence: presence);
} catch (error, stackTrace) {
if (!_initializedCompleter.isCompleted) {
_initializedCompleter.completeError(error, stackTrace);
@@ -1613,6 +1628,10 @@ class ChannelClientState {
_listenMemberUnbanned();
_listenUserStartWatching();
_listenUserStopWatching();
_startCleaningStaleTypingEvents();
_startCleaningStalePinnedMessages();
@@ -1754,6 +1773,39 @@ class ChannelClientState {
));
}
void _listenUserStartWatching() {
_subscriptions.add(
_channel.on(EventType.userWatchingStart).listen((event) {
final watcher = event.user;
if (watcher != null) {
final existingWatchers = channelState.watchers;
updateChannelState(channelState.copyWith(
watchers: [
...?existingWatchers,
watcher,
],
));
}
}),
);
}
void _listenUserStopWatching() {
_subscriptions.add(
_channel.on(EventType.userWatchingStop).listen((event) {
final watcher = event.user;
if (watcher != null) {
final existingWatchers = channelState.watchers;
updateChannelState(channelState.copyWith(
watchers: existingWatchers
?.where((user) => user.id != watcher.id)
.toList(growable: false),
));
}
}),
);
}
void _listenMemberUnbanned() {
_subscriptions.add(_channel
.on(EventType.userUnbanned)
@@ -2083,7 +2135,7 @@ class ChannelClientState {
channelStateStream.map((cs) => cs.watchers),
_channel.client.state.usersStream,
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(),
);
).distinct(const ListEquality().equals);
/// Channel member for the current user.
Member? get currentUserMember => members.firstWhereOrNull(
@@ -453,6 +453,15 @@ class StreamChatClient {
if (persistenceEnabled) {
await sync(cids: cids, lastSyncAt: _lastSyncedAt);
}
} else {
// channels are empty, assuming it's a fresh start
// and making sure `lastSyncAt` is initialized
if (persistenceEnabled) {
final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) {
await _chatPersistenceClient?.updateLastSyncAt(DateTime.now());
}
}
}
handleEvent(Event(
type: EventType.connectionRecovered,
@@ -6,6 +6,12 @@ enum PushProvider {
/// Send notifications using Google's Firebase Cloud Messaging
firebase,
/// Send notifications using Huawei's Push Kit
huawei,
/// Send notifications using Xiaomi's Mi Push Service
xiaomi,
/// Send notifications using Apple's Push Notification service
apn,
}
@@ -40,7 +40,7 @@ class SortOption<T> {
final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
final Comparator<T>? comparator;
/// Serialize model to json
@@ -74,6 +74,7 @@ class StreamChatNetworkError extends StreamChatError {
ChatErrorCode errorCode, {
int? statusCode,
this.data,
this.isRequestCancelledError = false,
}) : code = errorCode.code,
statusCode = statusCode ?? data?.statusCode,
super(errorCode.message);
@@ -84,6 +85,7 @@ class StreamChatNetworkError extends StreamChatError {
required String message,
this.statusCode,
this.data,
this.isRequestCancelledError = false,
}) : super(message);
///
@@ -96,10 +98,13 @@ class StreamChatNetworkError extends StreamChatError {
}
return StreamChatNetworkError.raw(
code: errorResponse?.code ?? -1,
message:
errorResponse?.message ?? response?.statusMessage ?? error.message,
message: errorResponse?.message ??
response?.statusMessage ??
error.message ??
'',
statusCode: errorResponse?.statusCode ?? response?.statusCode,
data: errorResponse,
isRequestCancelledError: error.type == DioErrorType.cancel,
)..stackTrace = error.stackTrace;
}
@@ -112,6 +117,9 @@ class StreamChatNetworkError extends StreamChatError {
/// Response body. please refer to [ErrorResponse].
final ErrorResponse? data;
/// True, in case the error is due to a cancelled network request.
final bool isRequestCancelledError;
StackTrace? _stackTrace;
///
@@ -87,8 +87,8 @@ class LoggingInterceptor extends Interceptor {
requestHeaders['contentType'] = options.contentType?.toString();
requestHeaders['responseType'] = options.responseType.toString();
requestHeaders['followRedirects'] = options.followRedirects;
requestHeaders['connectTimeout'] = options.connectTimeout;
requestHeaders['receiveTimeout'] = options.receiveTimeout;
requestHeaders['connectTimeout'] = options.connectTimeout?.toString();
requestHeaders['receiveTimeout'] = options.receiveTimeout?.toString();
_printMapAsTable(_logPrintRequest, requestHeaders, header: 'Headers');
_printMapAsTable(_logPrintRequest, options.extra, header: 'Extras');
}
@@ -101,7 +101,8 @@ class LoggingInterceptor extends Interceptor {
options.data as Map?,
header: 'Body',
);
} else if (data is FormData) {
}
if (data is FormData) {
final formDataMap = <String, dynamic>{}
..addEntries(data.fields)
..addEntries(data.files);
@@ -121,7 +122,7 @@ class LoggingInterceptor extends Interceptor {
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (error) {
if (err.type == DioErrorType.response) {
if (err.type == DioErrorType.badResponse) {
final uri = err.response?.requestOptions.uri;
_printBoxed(
_logPrintError,
@@ -162,7 +163,7 @@ class LoggingInterceptor extends Interceptor {
_logPrintResponse('');
_printResponse(_logPrintResponse, response);
_logPrintResponse('');
_printLine(_logPrintResponse, '');
_logPrintResponse('');
}
super.onResponse(response, handler);
}
@@ -29,8 +29,8 @@ class StreamHttpClient {
httpClient = dio ?? Dio() {
httpClient
..options.baseUrl = _options.baseUrl
..options.receiveTimeout = _options.receiveTimeout.inMilliseconds
..options.connectTimeout = _options.connectTimeout.inMilliseconds
..options.receiveTimeout = _options.receiveTimeout
..options.connectTimeout = _options.connectTimeout
..options.queryParameters = {
'api_key': apiKey,
..._options.queryParameters,
@@ -135,7 +135,6 @@ class Attachment extends Equatable {
late final UploadState uploadState;
/// Map of custom channel extraData
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// The attachment ID.
@@ -146,13 +145,13 @@ class Attachment extends Equatable {
/// Shortcut for file size.
///
/// {@macro fileSize}
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
int? get fileSize => extraData['file_size'] as int?;
/// Shortcut for file mimeType.
///
/// {@macro mimeType}
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
String? get mimeType => extraData['mime_type'] as String?;
/// Known top level fields.
@@ -52,7 +52,7 @@ class AttachmentFile {
/// Byte data for this file. Particularly useful if you want to manipulate
/// its data or easily upload to somewhere else.
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
final Uint8List? bytes;
/// The file size in bytes.
@@ -1,7 +1,7 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'attachment_file.dart';
@@ -43,10 +43,10 @@ mixin _$UploadState {
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
TResult? Function()? preparing,
TResult? Function(int uploaded, int total)? inProgress,
TResult? Function()? success,
TResult? Function(String error)? failed,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -68,10 +68,10 @@ mixin _$UploadState {
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
TResult? Function(Preparing value)? preparing,
TResult? Function(InProgress value)? inProgress,
TResult? Function(Success value)? success,
TResult? Function(Failed value)? failed,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -90,16 +90,18 @@ mixin _$UploadState {
abstract class $UploadStateCopyWith<$Res> {
factory $UploadStateCopyWith(
UploadState value, $Res Function(UploadState) then) =
_$UploadStateCopyWithImpl<$Res>;
_$UploadStateCopyWithImpl<$Res, UploadState>;
}
/// @nodoc
class _$UploadStateCopyWithImpl<$Res> implements $UploadStateCopyWith<$Res> {
class _$UploadStateCopyWithImpl<$Res, $Val extends UploadState>
implements $UploadStateCopyWith<$Res> {
_$UploadStateCopyWithImpl(this._value, this._then);
final UploadState _value;
// ignore: unused_field
final $Res Function(UploadState) _then;
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
}
/// @nodoc
@@ -110,14 +112,12 @@ abstract class _$$PreparingCopyWith<$Res> {
}
/// @nodoc
class __$$PreparingCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
class __$$PreparingCopyWithImpl<$Res>
extends _$UploadStateCopyWithImpl<$Res, _$Preparing>
implements _$$PreparingCopyWith<$Res> {
__$$PreparingCopyWithImpl(
_$Preparing _value, $Res Function(_$Preparing) _then)
: super(_value, (v) => _then(v as _$Preparing));
@override
_$Preparing get _value => super._value as _$Preparing;
: super(_value, _then);
}
/// @nodoc
@@ -162,10 +162,10 @@ class _$Preparing extends Preparing {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
TResult? Function()? preparing,
TResult? Function(int uploaded, int total)? inProgress,
TResult? Function()? success,
TResult? Function(String error)? failed,
}) {
return preparing?.call();
}
@@ -199,10 +199,10 @@ class _$Preparing extends Preparing {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
TResult? Function(Preparing value)? preparing,
TResult? Function(InProgress value)? inProgress,
TResult? Function(Success value)? success,
TResult? Function(Failed value)? failed,
}) {
return preparing?.call(this);
}
@@ -242,30 +242,30 @@ abstract class _$$InProgressCopyWith<$Res> {
factory _$$InProgressCopyWith(
_$InProgress value, $Res Function(_$InProgress) then) =
__$$InProgressCopyWithImpl<$Res>;
@useResult
$Res call({int uploaded, int total});
}
/// @nodoc
class __$$InProgressCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
class __$$InProgressCopyWithImpl<$Res>
extends _$UploadStateCopyWithImpl<$Res, _$InProgress>
implements _$$InProgressCopyWith<$Res> {
__$$InProgressCopyWithImpl(
_$InProgress _value, $Res Function(_$InProgress) _then)
: super(_value, (v) => _then(v as _$InProgress));
@override
_$InProgress get _value => super._value as _$InProgress;
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? uploaded = freezed,
Object? total = freezed,
Object? uploaded = null,
Object? total = null,
}) {
return _then(_$InProgress(
uploaded: uploaded == freezed
uploaded: null == uploaded
? _value.uploaded
: uploaded // ignore: cast_nullable_to_non_nullable
as int,
total: total == freezed
total: null == total
? _value.total
: total // ignore: cast_nullable_to_non_nullable
as int,
@@ -302,19 +302,18 @@ class _$InProgress extends InProgress {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$InProgress &&
const DeepCollectionEquality().equals(other.uploaded, uploaded) &&
const DeepCollectionEquality().equals(other.total, total));
(identical(other.uploaded, uploaded) ||
other.uploaded == uploaded) &&
(identical(other.total, total) || other.total == total));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(uploaded),
const DeepCollectionEquality().hash(total));
int get hashCode => Object.hash(runtimeType, uploaded, total);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$InProgressCopyWith<_$InProgress> get copyWith =>
__$$InProgressCopyWithImpl<_$InProgress>(this, _$identity);
@@ -332,10 +331,10 @@ class _$InProgress extends InProgress {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
TResult? Function()? preparing,
TResult? Function(int uploaded, int total)? inProgress,
TResult? Function()? success,
TResult? Function(String error)? failed,
}) {
return inProgress?.call(uploaded, total);
}
@@ -369,10 +368,10 @@ class _$InProgress extends InProgress {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
TResult? Function(Preparing value)? preparing,
TResult? Function(InProgress value)? inProgress,
TResult? Function(Success value)? success,
TResult? Function(Failed value)? failed,
}) {
return inProgress?.call(this);
}
@@ -422,13 +421,11 @@ abstract class _$$SuccessCopyWith<$Res> {
}
/// @nodoc
class __$$SuccessCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
class __$$SuccessCopyWithImpl<$Res>
extends _$UploadStateCopyWithImpl<$Res, _$Success>
implements _$$SuccessCopyWith<$Res> {
__$$SuccessCopyWithImpl(_$Success _value, $Res Function(_$Success) _then)
: super(_value, (v) => _then(v as _$Success));
@override
_$Success get _value => super._value as _$Success;
: super(_value, _then);
}
/// @nodoc
@@ -473,10 +470,10 @@ class _$Success extends Success {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
TResult? Function()? preparing,
TResult? Function(int uploaded, int total)? inProgress,
TResult? Function()? success,
TResult? Function(String error)? failed,
}) {
return success?.call();
}
@@ -510,10 +507,10 @@ class _$Success extends Success {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
TResult? Function(Preparing value)? preparing,
TResult? Function(InProgress value)? inProgress,
TResult? Function(Success value)? success,
TResult? Function(Failed value)? failed,
}) {
return success?.call(this);
}
@@ -552,24 +549,24 @@ abstract class Success extends UploadState {
abstract class _$$FailedCopyWith<$Res> {
factory _$$FailedCopyWith(_$Failed value, $Res Function(_$Failed) then) =
__$$FailedCopyWithImpl<$Res>;
@useResult
$Res call({String error});
}
/// @nodoc
class __$$FailedCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
class __$$FailedCopyWithImpl<$Res>
extends _$UploadStateCopyWithImpl<$Res, _$Failed>
implements _$$FailedCopyWith<$Res> {
__$$FailedCopyWithImpl(_$Failed _value, $Res Function(_$Failed) _then)
: super(_value, (v) => _then(v as _$Failed));
@override
_$Failed get _value => super._value as _$Failed;
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? error = freezed,
Object? error = null,
}) {
return _then(_$Failed(
error: error == freezed
error: null == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String,
@@ -603,16 +600,16 @@ class _$Failed extends Failed {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Failed &&
const DeepCollectionEquality().equals(other.error, error));
(identical(other.error, error) || other.error == error));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(error));
int get hashCode => Object.hash(runtimeType, error);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FailedCopyWith<_$Failed> get copyWith =>
__$$FailedCopyWithImpl<_$Failed>(this, _$identity);
@@ -630,10 +627,10 @@ class _$Failed extends Failed {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
TResult? Function()? preparing,
TResult? Function(int uploaded, int total)? inProgress,
TResult? Function()? success,
TResult? Function(String error)? failed,
}) {
return failed?.call(error);
}
@@ -667,10 +664,10 @@ class _$Failed extends Failed {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
TResult? Function(Preparing value)? preparing,
TResult? Function(InProgress value)? inProgress,
TResult? Function(Success value)? success,
TResult? Function(Failed value)? failed,
}) {
return failed?.call(this);
}
@@ -62,19 +62,19 @@ class ChannelModel {
final String type;
/// The cid of this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final String cid;
/// List of user permissions on this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final List<String>? ownCapabilities;
/// The channel configuration data
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final ChannelConfig config;
/// The user that created this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final User? createdBy;
/// True if this channel is frozen
@@ -82,23 +82,23 @@ class ChannelModel {
final bool frozen;
/// The date of the last message
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? lastMessageAt;
/// The date of channel creation
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime createdAt;
/// The date of the last channel update
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime updatedAt;
/// The date of channel deletion
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? deletedAt;
/// The count of this channel members
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final int memberCount;
/// The number of seconds in a cooldown
@@ -106,15 +106,15 @@ class ChannelModel {
final int cooldown;
/// True if the channel is disabled
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
bool? get disabled => extraData['disabled'] as bool?;
/// True if the channel is hidden
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
bool? get hidden => extraData['hidden'] as bool?;
/// The date of the last time channel got truncated
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
DateTime? get truncatedAt {
final truncatedAt = extraData['truncated_at'] as String?;
if (truncatedAt == null) return null;
@@ -122,11 +122,10 @@ class ChannelModel {
}
/// Map of custom channel extraData
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// The team the channel belongs to
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final String? team;
/// Known top level fields.
@@ -38,30 +38,11 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
cooldown: json['cooldown'] as int? ?? 0,
);
Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
final val = <String, dynamic>{
'id': instance.id,
'type': instance.type,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('cid', readonly(instance.cid));
writeNotNull('own_capabilities', readonly(instance.ownCapabilities));
writeNotNull('config', readonly(instance.config));
writeNotNull('created_by', readonly(instance.createdBy));
val['frozen'] = instance.frozen;
writeNotNull('last_message_at', readonly(instance.lastMessageAt));
writeNotNull('created_at', readonly(instance.createdAt));
writeNotNull('updated_at', readonly(instance.updatedAt));
writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('member_count', readonly(instance.memberCount));
val['cooldown'] = instance.cooldown;
val['extra_data'] = instance.extraData;
writeNotNull('team', readonly(instance.team));
return val;
}
Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'frozen': instance.frozen,
'cooldown': instance.cooldown,
'extra_data': instance.extraData,
};
@@ -95,14 +95,11 @@ class Message extends Equatable {
final String? text;
/// The status of a sending message.
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
final MessageSendingStatus status;
/// The message type.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
)
@JsonKey(includeToJson: false)
final String type;
/// The list of attachments, either provided by the user or generated from a
@@ -115,26 +112,26 @@ class Message extends Equatable {
final List<User> mentionedUsers;
/// A map describing the count of number of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final Map<String, int>? reactionCounts;
/// A map describing the count of score of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final Map<String, int>? reactionScores;
/// The latest reactions to the message created by any user.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final List<Reaction>? latestReactions;
/// The reactions added to the message by the current user.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final List<Reaction>? ownReactions;
/// The ID of the parent message, if the message is a thread reply.
final String? parentId;
/// A quoted reply message.
@JsonKey(toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final Message? quotedMessage;
final String? _quotedMessageId;
@@ -143,11 +140,11 @@ class Message extends Equatable {
String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id;
/// Reserved field indicating the number of replies for this message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final int? replyCount;
/// Reserved field indicating the thread participants for this message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final List<User>? threadParticipants;
/// Check if this message needs to show in the channel.
@@ -157,41 +154,38 @@ class Message extends Equatable {
final bool silent;
/// If true the message is shadowed.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
)
@JsonKey(includeToJson: false)
final bool shadowed;
/// A used command name.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final String? command;
final DateTime? _createdAt;
/// Reserved field indicating when the message was deleted.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? deletedAt;
/// Reserved field indicating when the message was created.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
DateTime get createdAt => _createdAt ?? DateTime.now();
final DateTime? _updatedAt;
/// Reserved field indicating when the message was updated last time.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
DateTime get updatedAt => _updatedAt ?? DateTime.now();
/// User who sent the message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final User? user;
/// If true the message is pinned.
final bool pinned;
/// Reserved field indicating when the message was pinned.
@JsonKey(toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? pinnedAt;
/// Reserved field indicating when the message will expire.
@@ -200,11 +194,10 @@ class Message extends Equatable {
final DateTime? pinExpires;
/// Reserved field indicating who pinned the message.
@JsonKey(toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final User? pinnedBy;
/// Message custom extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// True if the message is a system info.
@@ -217,7 +210,7 @@ class Message extends Equatable {
bool get isEphemeral => type == 'ephemeral';
/// A Map of translations.
@JsonKey(includeIfNull: false)
@JsonKey(includeToJson: false)
final Map<String, String>? i18n;
/// Known top level fields.
@@ -71,43 +71,16 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
),
);
Map<String, dynamic> _$MessageToJson(Message instance) {
final val = <String, dynamic>{
'id': instance.id,
'text': instance.text,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('type', readonly(instance.type));
val['attachments'] = instance.attachments.map((e) => e.toJson()).toList();
val['mentioned_users'] = User.toIds(instance.mentionedUsers);
writeNotNull('reaction_counts', readonly(instance.reactionCounts));
writeNotNull('reaction_scores', readonly(instance.reactionScores));
writeNotNull('latest_reactions', readonly(instance.latestReactions));
writeNotNull('own_reactions', readonly(instance.ownReactions));
val['parent_id'] = instance.parentId;
val['quoted_message'] = readonly(instance.quotedMessage);
val['quoted_message_id'] = instance.quotedMessageId;
writeNotNull('reply_count', readonly(instance.replyCount));
writeNotNull('thread_participants', readonly(instance.threadParticipants));
val['show_in_channel'] = instance.showInChannel;
val['silent'] = instance.silent;
writeNotNull('shadowed', readonly(instance.shadowed));
writeNotNull('command', readonly(instance.command));
writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('created_at', readonly(instance.createdAt));
writeNotNull('updated_at', readonly(instance.updatedAt));
writeNotNull('user', readonly(instance.user));
val['pinned'] = instance.pinned;
val['pinned_at'] = readonly(instance.pinnedAt);
val['pin_expires'] = instance.pinExpires?.toIso8601String();
val['pinned_by'] = readonly(instance.pinnedBy);
val['extra_data'] = instance.extraData;
writeNotNull('i18n', instance.i18n);
return val;
}
Map<String, dynamic> _$MessageToJson(Message instance) => <String, dynamic>{
'id': instance.id,
'text': instance.text,
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
'mentioned_users': User.toIds(instance.mentionedUsers),
'parent_id': instance.parentId,
'quoted_message_id': instance.quotedMessageId,
'show_in_channel': instance.showInChannel,
'silent': instance.silent,
'pinned': instance.pinned,
'pin_expires': instance.pinExpires?.toIso8601String(),
'extra_data': instance.extraData,
};
@@ -33,22 +33,21 @@ class Reaction {
final String type;
/// The date of the reaction
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime createdAt;
/// The user that sent the reaction
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final User? user;
/// The score of the reaction (ie. number of reactions sent)
final int score;
/// The userId that sent the reaction
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final String? userId;
/// Reaction custom extraData
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// Map of custom user extraData
@@ -20,22 +20,9 @@ Reaction _$ReactionFromJson(Map<String, dynamic> json) => Reaction(
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
);
Map<String, dynamic> _$ReactionToJson(Reaction instance) {
final val = <String, dynamic>{
'message_id': instance.messageId,
'type': instance.type,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('created_at', readonly(instance.createdAt));
writeNotNull('user', readonly(instance.user));
val['score'] = instance.score;
writeNotNull('user_id', readonly(instance.userId));
val['extra_data'] = instance.extraData;
return val;
}
Map<String, dynamic> _$ReactionToJson(Reaction instance) => <String, dynamic>{
'message_id': instance.messageId,
'type': instance.type,
'score': instance.score,
'extra_data': instance.extraData,
};
@@ -79,7 +79,7 @@ class User extends Equatable {
/// Shortcut for user name.
///
/// {@macro name}
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
String get name {
if (extraData.containsKey('name') && extraData['name'] != null) {
final name = extraData['name']! as String;
@@ -91,48 +91,39 @@ class User extends Equatable {
/// Shortcut for user image.
///
/// {@macro image}
@JsonKey(ignore: true)
@JsonKey(includeToJson: false, includeFromJson: false)
String? get image => extraData['image'] as String?;
/// User role.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final String? role;
/// User teams
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
)
@JsonKey(includeToJson: false)
final List<String> teams;
/// Date of user creation.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime createdAt;
/// Date of last user update.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime updatedAt;
/// Date of last user connection.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? lastActive;
/// True if user is online.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
)
@JsonKey(includeToJson: false)
final bool online;
/// True if user is banned from the chat.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
)
@JsonKey(includeToJson: false)
final bool banned;
/// The date at which the ban will expire.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@JsonKey(includeToJson: false)
final DateTime? banExpires;
/// The language this user prefers.
@@ -140,7 +131,6 @@ class User extends Equatable {
final String? language;
/// Map of custom user extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// List of users to list of userIds.
@@ -41,14 +41,6 @@ Map<String, dynamic> _$UserToJson(User instance) {
}
}
writeNotNull('role', readonly(instance.role));
writeNotNull('teams', readonly(instance.teams));
writeNotNull('created_at', readonly(instance.createdAt));
writeNotNull('updated_at', readonly(instance.updatedAt));
writeNotNull('last_active', readonly(instance.lastActive));
writeNotNull('online', readonly(instance.online));
writeNotNull('banned', readonly(instance.banned));
writeNotNull('ban_expires', readonly(instance.banExpires));
writeNotNull('language', instance.language);
val['extra_data'] = instance.extraData;
return val;
@@ -1,12 +1,5 @@
/// Used to avoid to serialize properties to json
// ignore: prefer_void_to_null
Null readonly(_) => null;
/// Helper class for serialization to and from json
class Serializer {
/// Used to avoid to serialize properties to json
static const Function readOnly = readonly;
/// Takes unknown json keys and puts them in the `extra_data` key
static Map<String, dynamic> moveToExtraDataFromRoot(
Map<String, dynamic> json,
@@ -36,6 +36,12 @@ class EventType {
/// Event sent when updating a message
static const String messageUpdated = 'message.updated';
/// Event sent when a user starts watching a channel
static const String userWatchingStart = 'user.watching.start';
/// Event sent when a user stops watching a channel
static const String userWatchingStop = 'user.watching.stop';
/// Event sent when reading a message
static const String messageRead = 'message.read';
+3 -30
View File
@@ -10,41 +10,13 @@ export 'package:logging/logging.dart' show Logger, Level, LogRecord;
export 'package:rate_limiter/rate_limiter.dart';
export 'package:uuid/uuid.dart';
export './src/core/api/attachment_file_uploader.dart';
export './src/core/api/requests.dart';
export './src/core/api/requests.dart';
export './src/core/api/responses.dart';
export './src/core/api/stream_chat_api.dart' show PushProvider;
export './src/core/error/error.dart';
export './src/core/models/action.dart';
export './src/core/models/attachment.dart';
export './src/core/models/attachment_file.dart';
export './src/core/models/channel_config.dart';
export './src/core/models/channel_model.dart';
export './src/core/models/channel_state.dart';
export './src/core/models/command.dart';
export './src/core/models/device.dart';
export './src/core/models/event.dart';
export './src/core/models/filter.dart' show Filter;
export './src/core/models/member.dart';
export './src/core/models/message.dart';
export './src/core/models/mute.dart';
export './src/core/models/own_user.dart';
export './src/core/models/reaction.dart';
export './src/core/models/read.dart';
export './src/core/models/user.dart';
export './src/core/util/extension.dart';
export './src/db/chat_persistence_client.dart';
export './src/event_type.dart';
export './src/permission_type.dart';
export './src/ws/connection_status.dart';
export 'src/client/channel.dart';
export 'src/client/client.dart';
export 'src/client/key_stroke_handler.dart';
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
export 'src/core/api/requests.dart';
export 'src/core/api/attachment_file_uploader.dart';
export 'src/core/api/requests.dart';
export 'src/core/api/responses.dart';
export 'src/core/api/stream_chat_api.dart' show PushProvider;
export 'src/core/api/stream_chat_api.dart';
export 'src/core/error/error.dart';
export 'src/core/models/action.dart';
@@ -68,4 +40,5 @@ export 'src/core/platform_detector/platform_detector.dart';
export 'src/core/util/extension.dart';
export 'src/db/chat_persistence_client.dart';
export 'src/event_type.dart';
export 'src/permission_type.dart';
export 'src/ws/connection_status.dart';
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '5.1.0';
const PACKAGE_VERSION = '5.3.0';
+21 -21
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 5.1.0
version: 5.3.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -9,26 +9,26 @@ environment:
sdk: '>=2.17.0 <3.0.0'
dependencies:
async: ^2.5.0
collection: ^1.15.0
dio: ^4.0.0
equatable: ^2.0.0
freezed_annotation: ^2.0.3
http_parser: ^4.0.0
jose: ^0.3.2
json_annotation: ^4.5.0
logging: ^1.0.1
meta: ^1.3.0
mime: ^1.0.0
rate_limiter: ^0.1.1
rxdart: ^0.27.0
uuid: ^3.0.4
web_socket_channel: ^2.0.0
async: ^2.10.0
collection: ^1.17.0
dio: ^5.1.1
equatable: ^2.0.5
freezed_annotation: ^2.2.0
http_parser: ^4.0.2
jose: ^0.3.3
json_annotation: ^4.8.0
logging: ^1.1.1
meta: ^1.8.0
mime: ^1.0.4
rate_limiter: ^1.0.0
rxdart: ^0.27.7
uuid: ^3.0.7
web_socket_channel: ^2.3.0
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
freezed: ^2.0.3
json_serializable: ^6.2.0
build_runner: ^2.3.3
dart_code_metrics: ^5.7.0
freezed: ^2.3.2
json_serializable: ^6.6.1
mocktail: ^0.3.0
test: ^1.17.12
test: ^1.24.1
+25 -101
View File
@@ -1,4 +1,3 @@
{
"channel": {
"id": "dev",
@@ -18,400 +17,325 @@
"text": "fasdfa",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f",
"text": "test message",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
"text": "test message",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35",
"text": "asdfadf",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
"text": "test",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc",
"text": "hi",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53",
"text": "fantastic",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240",
"text": "nice to meet you",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42",
"text": "hey",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa",
"text": "hello, everyone",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0",
"text": "who is there?",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356",
"text": "하이",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
"text": "what are you doing?",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4",
"text": "👍",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
"text": "sdasas",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d",
"text": "cjshsa",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
"text": "nhisagdhsadz",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3",
"text": "hvadhsahzd",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf",
"text": "hello",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73",
"text": "hello",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854",
"text": "hello",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480",
"text": "http://jaeger.ui.gtstrm.com/",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be",
"text": "hi",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40",
"text": "hi",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
},
{
"id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c",
"text": "😃",
"attachments": [],
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"silent": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
"pin_expires": null
}
],
"pinned_messages": [],
@@ -18,12 +18,9 @@
],
"mentioned_users": [],
"parent_id": "parentId",
"quoted_message": null,
"quoted_message_id": null,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null,
"show_in_channel": true,
"hey": "test"
}
@@ -516,6 +516,9 @@ void main() {
});
setUp(() async {
when(() => persistence.updateLastSyncAt(any()))
.thenAnswer((_) => Future.value());
when(persistence.getLastSyncAt).thenAnswer((_) async => null);
client = StreamChatClient(apiKey, chatApi: api, ws: ws)
..chatPersistenceClient = persistence;
await client.connectUser(user, token);
@@ -532,9 +535,12 @@ void main() {
test(
'''should update persistence connectionInfo and lastSync when sync succeeds''',
() async {
// persistence.updateLastSyncAt might be called
// when connecting the user.
// Resetting the logs so we start counting invocations correctly.
reset(persistence);
const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3'];
final lastSyncAt = DateTime.now();
when(() => api.general.sync(cids, lastSyncAt))
.thenAnswer((_) async => SyncResponse()
..events = [
@@ -567,6 +573,10 @@ void main() {
test(
'should work fine if persistence contains sync params',
() async {
// persistence.updateLastSyncAt might be called
// when connecting the user.
// Resetting the logs so we start counting invocations correctly.
reset(persistence);
const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3'];
final lastSyncAt = DateTime.now();
@@ -22,26 +22,37 @@ void main() {
test('addDevice should work', () async {
const deviceId = 'test-device-id';
const pushProvider = PushProvider.firebase;
const pushProvidersMap = {
'apn': PushProvider.apn,
'firebase': PushProvider.firebase,
'huawei': PushProvider.huawei,
'xiaomi': PushProvider.xiaomi,
};
const path = '/devices';
when(() => client.post(
path,
data: {
'id': deviceId,
'push_provider': pushProvider.name,
},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
for (final pushProviderMapEntry in pushProvidersMap.entries) {
final data = {
'id': deviceId,
'push_provider': pushProviderMapEntry.key,
};
when(() {
return client.post(
path,
data: data,
);
}).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await deviceApi.addDevice(deviceId, pushProvider);
final res =
await deviceApi.addDevice(deviceId, pushProviderMapEntry.value);
expect(res, isNotNull);
expect(res, isNotNull);
verify(() => client.post(path, data: any(named: 'data'))).called(1);
verify(() => client.post(path, data: data)).called(1);
}
verifyNoMoreInteractions(client);
expect(pushProvidersMap.length, PushProvider.values.length,
reason: 'All PushProvider should be tested');
});
test('addDevice should work with pushProviderName', () async {
@@ -394,7 +394,12 @@ void main() {
path,
data: {'language': language},
)).thenAnswer((_) async => successResponse(path, data: {
'message': translatedMessage.toJson(),
'message': {
...translatedMessage.toJson(),
'i18n': {
language: translatedMessageText,
},
},
}));
final res = await messageApi.translateMessage(messageId, language);
@@ -129,7 +129,11 @@ void main() {
await client.get('path');
} on StreamChatNetworkError catch (e) {
expect(e, isA<StreamChatNetworkError>());
expect(e.message, "Dio can't establish new connection after closed.");
expect(
e.message,
"The connection errored: Dio can't establish a new connection"
' after it was closed.',
);
}
});
+1 -1
View File
@@ -50,7 +50,7 @@ pubspec.lock
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
# Avoid committing generated JavaScript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
+59 -8
View File
@@ -1,9 +1,60 @@
## Upcoming
🐞 Fixed
- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for images.
- [#1475](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation.
✅ Added
- Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line.
- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to customize `AttachmentActionsModal`.
- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to customize the keys used to send and clear the quoted message.
🔄 Changed
- Updated dependencies to resolvable versions.
🚀 Improved
-
- Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455)
## 5.3.0
🔄 Changed
- Updated `photo_manager` dependency to `^2.5.2`
🐞 Fixed
- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces.
- Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser.
- Fixed uploading files on Windows - fixed temp file path.
✅ Added
- New `noPhotoOrVideoLabel` displayed when there is no files to choose.
## 5.2.0
✅ Added
- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization.
🔄 Changed
- Updated `lottie` dependency to `^2.0.0`
- Updated `desktop_drop` dependency to `^0.4.0`
- Updated `connectivity_plus` dependency to `^3.0.2`
- Updated `dart_vlc` dependency to `^0.4.0`
- Updated `file_picker` dependency to `^5.2.4`
- Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
- Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
🐞 Fixed
- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading.
- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default behaviour and allows `TextOverflow`.
- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on web.
- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`.
## 5.1.0
🐞 Fixed
@@ -36,7 +87,7 @@
✅ Added
- Added `StreamMemberGridView` and `StreamMemberListView`.
- Added support for additional text field params in `StreamMessageInput`
- Added support for additional text field parameters in `StreamMessageInput`
* `maxLines`
* `minLines`
* `textInputAction`
@@ -221,7 +272,7 @@
- Fix commands resetting the `StreamMessageInputController.value`.
- [[#996]](https://github.com/GetStream/stream-chat-flutter/issues/996) Videos break bottom photo
carousal.
- Fix: URLs with path and/or query params are not enriched.
- Fix: URLs with path and/or query parameters are not enriched.
- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to
access gallery when opening the file picker.
@@ -885,11 +936,11 @@ typedef MessageBuilder = Widget Function(
## 1.0.0-beta
- **Refreshed widgets design**
- Improved api documentation
- Improved API documentation
- Updated `stream_chat` dependency to `^1.0.0-beta`
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
- Reimplemented existing widgets
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
- Extracted sample app into dedicated [repository](https://github.com/GetStream/flutter-samples)
- Re-implemented existing widgets
using [`stream_chat_flutter_core`](https://pub.dev/packages/stream_chat_flutter_core)
## 0.2.21
@@ -898,7 +949,7 @@ typedef MessageBuilder = Widget Function(
## 0.2.20+4
- Fix channelPreview when the message list is empty
- Fix `channelPreview` when the message list is empty
## 0.2.20+3
@@ -1310,4 +1361,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
## 0.0.1
- First release
- First release
+10 -9
View File
@@ -32,7 +32,7 @@ It teaches you how to use this SDK and also shows how to make frequently require
## Example App
This repo includes a fully functional example app with setup instructions.
This repository includes a fully functional example app with setup instructions.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter/example) folder.
## Add dependency
@@ -46,8 +46,9 @@ You should then run `flutter packages get`
### Android
The package uses [photo_manager](https://pub.dev/packages/photo_manager) to access the device's photo library.
The package uses [`photo_manager`](https://pub.dev/packages/photo_manager) to access the device's photo library.
Follow [this wiki](https://pub.dev/packages/photo_manager#android-10-q-29) to fulfil the Android requirements.
You need to take additional steps if you are targeting Android 13. Read [this section](https://pub.dev/packages/photo_manager#android-13-api-33-extra-configs) for more information.
### iOS
@@ -55,9 +56,9 @@ The library uses [flutter file picker plugin](https://github.com/miguelpruivo/fl
files from the os.
Follow [this wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/Setup#ios) to fulfill iOS requirements.
We also use [video_player](https://pub.dev/packages/video_player) to reproduce videos. Follow [this guide](https://pub.dev/packages/video_player#installation) to fulfill the requirements.
We also use [`video_player`](https://pub.dev/packages/video_player) to reproduce videos. Follow [this guide](https://pub.dev/packages/video_player#installation) to fulfill the requirements.
To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin.
To pick images from the camera, we use the [`image_picker`](https://pub.dev/packages/image_picker) plugin.
Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements.
### Web
@@ -70,7 +71,7 @@ For the web, edit your `index.html` and add the following in the `<body>` tag in
### MacOS
For MacOS use the [file_selector](https://pub.dev/packages/file_selector#macos) package.
For MacOS use the [`file_selector`](https://pub.dev/packages/file_selector#macos) package.
Follow [these instructions](https://pub.dev/packages/file_selector#macos) to check the requirements.
You also need to add the following [entitlement](https://docs.flutter.dev/development/platform-integration/desktop#entitlements-and-the-app-sandbox):
@@ -88,8 +89,8 @@ If it seems related to the [flutter file picker plugin](https://github.com/migue
## Docs
This package provides UI components required for integrating Stream Chat into your application.
Alternatively, you may use the core package [stream_chat_flutter_core](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter_core) which allows more customisation and provides business logic but no UI components.
If you require the maximum amount of control over the API, please use the low level client package: [stream_chat](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat).
Alternatively, you may use the core package [`stream_chat_flutter_core`](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter_core) which allows more customisation and provides business logic but no UI components.
If you require the maximum amount of control over the API, please use the low level client package: [`stream_chat`](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat).
### UI Components
@@ -175,7 +176,7 @@ Out of the box, all chat widgets use their default styling, and there are two wa
### Offline storage
To add data persistance you can extend the class `ChatPersistenceClient` and pass an instance to the `StreamChatClient`.
To add data persistence you can extend the class `ChatPersistenceClient` and pass an instance to the `StreamChatClient`.
```dart
class CustomChatPersistentClient extends ChatPersistenceClient {
@@ -207,7 +208,7 @@ final client = StreamChatClient(
## Contributing
We welcome code changes that improve this library or fix a problem,
please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github.
please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on GitHub.
We are pleased to merge your code into the official repository.
Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first.
See our license file for more details.
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 31
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -41,7 +41,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 23
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -9,17 +9,20 @@
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application
android:name=".Application"
android:label="example"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.6.0'
ext.kotlin_version = '1.7.21'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -199,6 +199,7 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -235,6 +236,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -61,5 +61,7 @@
<string>Explain why your app uses the mic</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -177,7 +177,7 @@ class _SplitViewState extends State<SplitView> {
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
@@ -300,6 +300,7 @@ class _ChannelPageState extends State<ChannelPage> {
@override
void dispose() {
focusNode.dispose();
messageInputController.dispose();
super.dispose();
}
}
@@ -78,7 +78,7 @@ class _SplitViewState extends State<SplitView> {
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
@@ -21,7 +21,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// - We set up the Chat [StreamChatClient] with the API key
///
/// - We set the the current user for Chat with [StreamChatClient.connectUser]
/// - We set the current user for Chat with [StreamChatClient.connectUser]
/// and a pre-generated user token
///
/// - We make [StreamChat] the root Widget of our application
@@ -74,6 +74,9 @@ class MyApp extends StatelessWidget {
fit: BoxFit.cover,
),
),
ownMessageTheme: const StreamMessageThemeData(
urlAttachmentTitleMaxLine: 1,
),
otherMessageTheme: StreamMessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle(
@@ -82,6 +85,7 @@ class MyApp extends StatelessWidget {
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(8),
),
urlAttachmentTitleMaxLine: 1,
),
).merge(defaultTheme);
@@ -9,10 +9,8 @@
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
@@ -24,16 +22,10 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}
@@ -6,10 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
desktop_drop
file_selector_linux
screen_retriever
sqlite3_flutter_libs
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
@@ -203,7 +203,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0930;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
@@ -256,6 +256,7 @@
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -404,7 +405,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -484,7 +485,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -531,7 +532,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -19,6 +19,7 @@ version: 1.0.0+1
environment:
sdk: '>=2.17.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
# The following adds the Cupertino Icons font to your application.
@@ -27,7 +28,7 @@ dependencies:
cupertino_icons: ^1.0.4
flutter:
sdk: flutter
responsive_builder: ^0.4.2
responsive_builder: ^0.6.4
stream_chat_flutter:
path: ../
stream_chat_localizations:
@@ -6,16 +6,14 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_native_view/flutter_native_view_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <thumblr_windows/thumblr_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
@@ -26,16 +24,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterNativeViewPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterNativeViewPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
ThumblrWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ThumblrWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
@@ -3,16 +3,14 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
connectivity_plus
dart_vlc
desktop_drop
file_selector_windows
flutter_native_view
screen_retriever
share_plus
sqlite3_flutter_libs
thumblr_windows
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
@@ -1,5 +1,4 @@
export 'attachment_error.dart';
export 'attachment_error.dart';
export 'attachment_upload_state_builder.dart';
export 'attachment_widget.dart' show AttachmentSource;
export 'file_attachment.dart';
@@ -195,7 +195,7 @@ class _FileTypeImage extends StatelessWidget {
shape: _getDefaultShape(context),
child: source.when(
local: () => StreamVideoThumbnailImage(
video: attachment.file!.path!,
video: attachment.file!.path,
placeholderBuilder: (_) => const Center(
child: SizedBox(
width: 20,
@@ -205,7 +205,7 @@ class _FileTypeImage extends StatelessWidget {
),
),
network: () => StreamVideoThumbnailImage(
video: attachment.assetUrl!,
video: attachment.assetUrl,
placeholderBuilder: (_) => const Center(
child: SizedBox(
width: 20,
@@ -17,6 +17,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
this.onShowMessage,
this.onReplyMessage,
this.onAttachmentTap,
this.attachmentActionsModalBuilder,
});
/// {@macro showMessageCallback}
@@ -28,6 +29,9 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
final imageUrl =
@@ -260,6 +264,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
);
},
@@ -123,7 +123,9 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
final tempDir = await getTemporaryDirectory();
final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows);
final tempFilePath = tempPath.resolve(fileName!).path;
final tempFilePath = tempPath
.resolve(fileName!)
.toFilePath(windows: CurrentPlatform.isWindows);
print(tempFilePath);
final attachmentFileBytes = attachmentFile.bytes;
@@ -22,6 +22,7 @@ class StreamImageAttachment extends StreamAttachmentWidget {
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
this.attachmentActionsModalBuilder,
});
/// The [StreamMessageThemeData] to use for the image title
@@ -52,26 +53,40 @@ class StreamImageAttachment extends StreamAttachmentWidget {
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.localUri == null || attachment.file?.bytes == null) {
return AttachmentError(constraints: constraints);
}
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
if (attachment.file?.bytes != null) {
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: _imageErrorBuilder,
),
),
);
);
} else if (attachment.localUri != null) {
return _buildImageAttachment(
context,
Image.asset(
attachment.localUri!.path,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: _imageErrorBuilder,
),
);
} else {
return AttachmentError(
constraints: constraints,
);
}
},
network: () {
var imageUrl =
@@ -116,6 +131,12 @@ class StreamImageAttachment extends StreamAttachmentWidget {
);
}
Widget _imageErrorBuilder(BuildContext _, Object __, StackTrace? ___) =>
Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
return Container(
constraints: constraints,
@@ -144,6 +165,8 @@ class StreamImageAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
);
},
@@ -18,6 +18,7 @@ class StreamImageGroup extends StatelessWidget {
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
this.attachmentActionsModalBuilder,
});
/// List of attachments to show
@@ -54,6 +55,9 @@ class StreamImageGroup extends StatelessWidget {
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@@ -154,6 +158,7 @@ class StreamImageGroup extends StatelessWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
),
),
@@ -170,6 +175,7 @@ class StreamImageGroup extends StatelessWidget {
imageThumbnailSize: imageThumbnailSize,
imageThumbnailResizeType: imageThumbnailResizeType,
imageThumbnailCropType: imageThumbnailCropType,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
);
}
}
@@ -107,7 +107,7 @@ class StreamUrlAttachment extends StatelessWidget {
if (urlAttachment.title != null)
Text(
urlAttachment.title!.trim(),
maxLines: 1,
maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w700),
@@ -17,6 +17,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
this.onShowMessage,
this.onReplyMessage,
this.onAttachmentTap,
this.attachmentActionsModalBuilder,
});
/// The [StreamMessageThemeData] to use for the title
@@ -31,6 +32,9 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return source.when(
@@ -41,7 +45,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.file!.path!,
video: attachment.file!.path,
thumbUrl: attachment.thumbUrl,
constraints: constraints,
),
@@ -54,7 +58,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.assetUrl!,
video: attachment.assetUrl,
thumbUrl: attachment.thumbUrl,
constraints: constraints,
),
@@ -86,6 +90,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
),
),
@@ -33,10 +33,10 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to download [attachment].
final AttachmentDownloader? attachmentDownloader;
/// Show reply option
/// Show "reply" option
final bool showReply;
/// Show show in chat option
/// Show "show in chat" option
final bool showShowInChat;
/// Show save option
@@ -55,6 +55,7 @@ class AttachmentActionsModal extends StatelessWidget {
Attachment? attachment,
Message? message,
VoidCallback? onShowMessage,
VoidCallback? onReply,
AttachmentDownloader? attachmentDownloader,
bool? showReply,
bool? showShowInChat,
@@ -67,6 +68,7 @@ class AttachmentActionsModal extends StatelessWidget {
attachment: attachment ?? this.attachment,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
onReply: onReply ?? this.onReply,
attachmentDownloader: attachmentDownloader ?? this.attachmentDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
@@ -112,7 +114,7 @@ class AttachmentActionsModal extends StatelessWidget {
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() => Navigator.of(context).pop(ReturnActionType.reply),
onReply,
),
if (showShowInChat)
_buildButton(
@@ -162,8 +162,8 @@ class StreamChannelHeader extends StatelessWidget
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -131,8 +131,8 @@ class StreamChannelListHeader extends StatelessWidget
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -181,7 +181,10 @@ class StreamChannelListHeader extends StatelessWidget
package: 'stream_chat_flutter',
width: 24,
height: 24,
color: color,
colorFilter: ColorFilter.mode(
color,
BlendMode.srcIn,
),
);
},
),
@@ -1,4 +1,8 @@
// TODO: remove in v6 as this is no longer used. Currently exported.
/// Return action for coming back from pages
@Deprecated('''
ReturnActionType has been deprecated and is no longer used.''')
enum ReturnActionType {
/// No return action
none,
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
@@ -61,6 +60,7 @@ class StreamGalleryFooter extends StatefulWidget
class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
final shareButtonKey = GlobalKey();
@override
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
@@ -108,12 +108,19 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
await file.writeAsBytes(bytes);
final box =
shareButtonKey.currentContext?.findRenderObject();
final size = shareButtonKey.currentContext?.size;
final position =
(box! as RenderBox).localToGlobal(Offset.zero);
await Share.shareXFiles(
[XFile(filePath)],
sharePositionOrigin:
Rect.fromLTWH(position.dx, position.dy, 0, 0),
sharePositionOrigin: Rect.fromLTWH(
position.dx,
position.dy,
size?.width ?? 50,
(size?.height ?? 2) / 2,
),
);
},
),
@@ -219,8 +226,8 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
child: AspectRatio(
aspectRatio: 1,
child: StreamVideoThumbnailImage(
video: (attachment.file?.path ??
attachment.assetUrl)!,
video:
attachment.file?.path ?? attachment.assetUrl,
),
),
),
@@ -80,8 +80,8 @@ class StreamGalleryHeader extends StatelessWidget
final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context);
final theme = Theme.of(context);
return AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -18,6 +18,9 @@ abstract class Translations {
/// The label for showing no users
String get noUsersLabel;
/// The label for showing no photo or video
String get noPhotoOrVideoLabel;
/// The text for showing user is online
String get userOnlineText;
@@ -78,7 +81,10 @@ abstract class Translations {
/// The text for showing the unread messages count
/// in the [StreamMessageListView]
String unreadMessagesSeparatorText(int unreadCount);
String unreadMessagesSeparatorText(
@Deprecated('unreadCount is not used anymore and will be removed ')
int unreadCount,
);
/// The label for "connected" in [StreamConnectionStatusBuilder]
String get connectedLabel;
@@ -373,6 +379,9 @@ class DefaultTranslations implements Translations {
@override
String get noUsersLabel => 'There are no users currently';
@override
String get noPhotoOrVideoLabel => 'There is no photo or video';
@override
String get retryLabel => 'Retry';
@@ -791,12 +800,7 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments
String get linkDisabledError => 'Links are disabled';
@override
String unreadMessagesSeparatorText(int unreadCount) {
if (unreadCount == 1) {
return '1 unread message';
}
return '$unreadCount unread messages';
}
String unreadMessagesSeparatorText(int unreadCount) => 'New messages';
@override
String get enableFileAccessMessage => 'Please enable access to files'
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:stream_chat_flutter/src/message_actions_modal/mam_widgets.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template messageActionsModal}
@@ -97,34 +98,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Widget _showMessageOptionsModal() {
final mediaQueryData = MediaQuery.of(context);
final size = mediaQueryData.size;
final user = StreamChat.of(context).currentUser;
final orientation = mediaQueryData.orientation;
final roughMaxSize = size.width * 2 / 3;
var messageTextLength = widget.message.text!.length;
if (widget.message.quotedMessage != null) {
var quotedMessageLength =
(widget.message.quotedMessage!.text?.length ?? 0) + 40;
if (widget.message.quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize = messageTextLength *
(widget.messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2;
final divFactor = widget.message.attachments.isNotEmpty
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final fontSize = widget.messageTheme.messageTextStyle?.fontSize;
final streamChatThemeData = StreamChatTheme.of(context);
final numberOfReactions =
StreamChatConfiguration.of(context).reactionIcons.length;
final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
final channel = StreamChannel.of(context).channel;
final child = Center(
@@ -142,11 +121,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
builder: (context, constraints) {
return Align(
alignment: Alignment(
_calculateReactionsHorizontalAlignmentValue(
calculateReactionsHorizontalAlignment(
user,
divFactor,
shiftFactor,
widget.message,
constraints,
fontSize,
orientation,
),
0,
),
@@ -156,7 +136,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
);
},
),
const SizedBox(height: 8),
const SizedBox(height: 10),
IgnorePointer(
child: widget.messageWidget,
),
@@ -281,93 +261,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
);
}
double _calculateReactionsHorizontalAlignmentValue(
User? user,
num divFactor,
double shiftFactor,
BoxConstraints constraints,
) {
var result = 0.0;
var cont = true;
if (user?.id == widget.message.user?.id) {
if (divFactor >= 1.0) {
// This calculation is hacky and does not cover all bases!!!
// A better option is needed!
// Landscape calculations
if (constraints.maxWidth == 1350) {
// 12.7 iPad Pro
result = shiftFactor + 0.5;
cont = false;
} else if (constraints.maxWidth == 1178) {
// 11 inch iPad Pro
result = shiftFactor + 0.42;
cont = false;
} else if (constraints.maxWidth == 1164) {
// iPad Air 4
result = shiftFactor + 0.4;
cont = false;
} else if (constraints.maxWidth == 1117) {
// iPad Mini 6
result = shiftFactor + 0.37;
cont = false;
} else if (constraints.maxWidth == 1064) {
// iPad 9th gen
result = shiftFactor + 0.33;
cont = false;
} else if (constraints.maxWidth == 1008) {
// 9.7 inch iPad Pro
result = shiftFactor + 0.3;
cont = false;
} else if (constraints.maxWidth >= 200 && constraints.maxWidth <= 400) {
// Phone (?)
result = shiftFactor - 0.2;
cont = false;
}
if (cont) {
// Portrait calculations
if (constraints.maxWidth == 1008) {
// 12.7 iPad Pro
result = shiftFactor + 0.3;
} else if (constraints.maxWidth == 818) {
// 11 inch iPad Pro
result = shiftFactor + 0.07;
} else if (constraints.maxWidth == 804) {
// iPad Air 4
result = shiftFactor + 0.04;
} else if (constraints.maxWidth == 794) {
// iPad 9th gen
result = shiftFactor + 0.02;
} else if (constraints.maxWidth >= 752) {
// 9.7 inch iPad Pro
result = shiftFactor - 0.05;
} else if (constraints.maxWidth == 728) {
// iPad Mini 6
result = shiftFactor - 0.1;
}
}
} else {
result = 1.2 - divFactor;
}
} else {
if (divFactor >= 1.0) {
result = shiftFactor + 0.2;
} else {
result = -(1.2 - divFactor);
}
}
// Ensure reactions don't get pushed past the edge of the screen.
//
// Hacky!!! Needs improvement!!!
if (result > 1) {
return 1;
} else {
return result;
}
}
InkWell _buildCustomAction(
BuildContext context,
StreamMessageAction messageAction,
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Shows a modal material design bottom sheet.
@@ -107,44 +107,34 @@ Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
controller: controller,
initialAttachments: initialAttachments,
builder: (context, controller, child) {
return PlatformWidget(
web: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
mobile: (context) {
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
desktop: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
final currentPlatform = defaultTargetPlatform;
final isWebOrDesktop = kIsWeb ||
currentPlatform == TargetPlatform.macOS ||
currentPlatform == TargetPlatform.linux ||
currentPlatform == TargetPlatform.windows;
if (isWebOrDesktop) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
}
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
);
@@ -276,7 +276,7 @@ class _ParseAttachments extends StatelessWidget {
'video': (_, attachment) {
return StreamVideoThumbnailImage(
key: ValueKey(attachment.assetUrl),
video: attachment.file?.path ?? attachment.assetUrl!,
video: attachment.file?.path ?? attachment.assetUrl,
constraints: BoxConstraints.loose(const Size(32, 32)),
errorBuilder: (_, __) => AttachmentError(
constraints: BoxConstraints.loose(const Size(32, 32)),
@@ -1220,7 +1220,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
104,
),
),
video: (attachment.file?.path ?? attachment.assetUrl)!,
video: attachment.file?.path ?? attachment.assetUrl,
),
Positioned(
left: 8,
@@ -80,7 +80,6 @@ class StreamMessageTextField extends StatefulWidget {
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '',
@@ -155,31 +154,7 @@ class StreamMessageTextField extends StatefulWidget {
keyboardType = keyboardType ??
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection =
enableInteractiveSelection ?? (!readOnly || !obscureText),
toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true,
paste: true,
))
: (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
)));
enableInteractiveSelection ?? (!readOnly || !obscureText);
/// Controls the message being edited.
///
@@ -304,13 +279,6 @@ class StreamMessageTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.readOnly}
final bool readOnly;
/// Configuration of toolbar options.
///
/// If not set, select all and paste will default to be enabled. Copy and cut
/// will be disabled if [obscureText] is true. If [readOnly] is true,
/// paste and cut will be disabled regardless.
final ToolbarOptions toolbarOptions;
/// {@macro flutter.widgets.editableText.showCursor}
final bool? showCursor;
@@ -720,7 +688,6 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
textAlignVertical: widget.textAlignVertical,
textDirection: widget.textDirection,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
@@ -24,9 +24,7 @@ class UnreadMessagesSeparator extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
context.translations.unreadMessagesSeparatorText(
unreadCount,
),
context.translations.unreadMessagesSeparatorText(unreadCount),
textAlign: TextAlign.center,
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
),
@@ -32,6 +32,7 @@ class BottomRow extends StatelessWidget {
this.deletedBottomRowBuilder,
this.onThreadTap,
this.usernameBuilder,
this.sendingIndicatorBuilder,
});
/// {@macro messageIsDeleted}
@@ -88,6 +89,61 @@ class BottomRow extends StatelessWidget {
/// {@macro usernameBuilder}
final Widget Function(BuildContext, Message)? usernameBuilder;
/// {@macro sendingIndicatorBuilder}
final Widget Function(BuildContext, Message)? sendingIndicatorBuilder;
/// {@template copyWith}
/// Creates a copy of [BottomRow] with specified attributes
/// overridden.
/// {@endtemplate}
BottomRow copyWith({
Key? key,
bool? isDeleted,
Message? message,
bool? showThreadReplyIndicator,
bool? showInChannel,
bool? showTimeStamp,
bool? showUsername,
bool? reverse,
bool? showSendingIndicator,
bool? hasUrlAttachments,
bool? isGiphy,
bool? isOnlyEmoji,
StreamMessageThemeData? messageTheme,
StreamChatThemeData? streamChatTheme,
bool? hasNonUrlAttachments,
StreamChatState? streamChat,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(Message)? onThreadTap,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? sendingIndicatorBuilder,
}) =>
BottomRow(
key: key ?? this.key,
isDeleted: isDeleted ?? this.isDeleted,
message: message ?? this.message,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannel: showInChannel ?? this.showInChannel,
showTimeStamp: showTimeStamp ?? this.showTimeStamp,
showUsername: showUsername ?? this.showUsername,
reverse: reverse ?? this.reverse,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
hasUrlAttachments: hasUrlAttachments ?? this.hasUrlAttachments,
isGiphy: isGiphy ?? this.isGiphy,
isOnlyEmoji: isOnlyEmoji ?? this.isOnlyEmoji,
messageTheme: messageTheme ?? this.messageTheme,
streamChatTheme: streamChatTheme ?? this.streamChatTheme,
hasNonUrlAttachments: hasNonUrlAttachments ?? this.hasNonUrlAttachments,
streamChat: streamChat ?? this.streamChat,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
sendingIndicatorBuilder:
sendingIndicatorBuilder ?? this.sendingIndicatorBuilder,
);
@override
Widget build(BuildContext context) {
if (isDeleted) {
@@ -147,13 +203,14 @@ class BottomRow extends StatelessWidget {
),
if (showSendingIndicator)
WidgetSpan(
child: SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
child: sendingIndicatorBuilder?.call(context, message) ??
SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
),
]);
@@ -40,7 +40,8 @@ class StreamMessageText extends StatelessWidget {
.translate(language)
.replaceMentions()
.text
?.replaceAll('\n', '\n\n');
?.replaceAll('\n', '\n\n')
.trim();
final themeData = Theme.of(context);
return MarkdownBody(
data: messageText ?? '',
@@ -8,6 +8,7 @@ import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction
import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart';
import 'package:stream_chat_flutter/src/dialogs/dialogs.dart';
import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart';
import 'package:stream_chat_flutter/src/message_widget/bottom_row.dart';
import 'package:stream_chat_flutter/src/message_widget/message_widget_content.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/message_reactions_modal.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -79,8 +80,15 @@ class StreamMessageWidget extends StatefulWidget {
this.userAvatarBuilder,
this.editMessageInputBuilder,
this.textBuilder,
this.bottomRowBuilder,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.customAttachmentBuilders,
this.padding,
this.textPadding = const EdgeInsets.symmetric(
@@ -92,11 +100,19 @@ class StreamMessageWidget extends StatefulWidget {
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
this.usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
this.imageAttachmentThumbnailSize = const Size(400, 400),
this.imageAttachmentThumbnailResizeType = 'clip',
this.imageAttachmentThumbnailCropType = 'center',
}) : attachmentBuilders = {
this.attachmentActionsModalBuilder,
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
),
attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
side: attachmentBorderSide ??
@@ -129,6 +145,8 @@ class StreamMessageWidget extends StatefulWidget {
imageThumbnailResizeType:
imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
),
attachmentShape: border,
@@ -156,6 +174,7 @@ class StreamMessageWidget extends StatefulWidget {
imageThumbnailSize: imageAttachmentThumbnailSize,
imageThumbnailResizeType: imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
attachmentShape: border,
);
@@ -189,6 +208,8 @@ class StreamMessageWidget extends StatefulWidget {
onAttachmentTap(message, attachment);
}
: null,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
);
}).toList(),
),
@@ -306,7 +327,13 @@ class StreamMessageWidget extends StatefulWidget {
/// {@template bottomRowBuilder}
/// Widget builder for building a bottom row below the message
/// {@endtemplate}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@template bottomRowBuilderWithDefaultWidget}
/// Widget builder for building a bottom row below the message.
/// Also contains the default bottom row widget.
/// {@endtemplate}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@template deletedBottomRowBuilder}
/// Widget builder for building a bottom row below a deleted message
@@ -512,6 +539,9 @@ class StreamMessageWidget extends StatefulWidget {
/// {@macro onMessageWidgetAttachmentTap}
final OnMessageWidgetAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
/// Size of the image attachment thumbnail.
final Size imageAttachmentThumbnailSize;
@@ -537,9 +567,19 @@ class StreamMessageWidget extends StatefulWidget {
void Function(Message)? onReplyTap,
Widget Function(BuildContext, Message)? editMessageInputBuilder,
Widget Function(BuildContext, Message)? textBuilder,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? bottomRowBuilder,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') BottomRowBuilder? bottomRowBuilder,
BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
StreamMessageThemeData? messageTheme,
@@ -586,7 +626,31 @@ class StreamMessageWidget extends StatefulWidget {
Size? imageAttachmentThumbnailSize,
String? imageAttachmentThumbnailResizeType,
String? imageAttachmentThumbnailCropType,
AttachmentActionsBuilder? attachmentActionsModalBuilder,
}) {
assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
var _bottomRowBuilderWithDefaultWidget =
bottomRowBuilderWithDefaultWidget ??
this.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??= (context, message, defaultWidget) {
final _bottomRowBuilder = bottomRowBuilder ?? this.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
);
};
return StreamMessageWidget(
key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap,
@@ -595,10 +659,7 @@ class StreamMessageWidget extends StatefulWidget {
editMessageInputBuilder:
editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget,
onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme,
@@ -652,6 +713,8 @@ class StreamMessageWidget extends StatefulWidget {
this.imageAttachmentThumbnailResizeType,
imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ??
this.imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder ?? this.attachmentActionsModalBuilder,
);
}
@@ -838,52 +901,69 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
? Alignment.centerRight
: Alignment.centerLeft,
widthFactor: widget.widthFactor,
child: MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
onThreadTap: widget.onThreadTap,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilder: widget.bottomRowBuilder,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
usernameBuilder: widget.usernameBuilder,
),
child: Builder(builder: (context) {
var _bottomRowBuilderWithDefaultWidget =
widget.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??=
(context, message, defaultWidget) {
final _bottomRowBuilder = widget.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: widget.onThreadTap,
usernameBuilder: widget.usernameBuilder,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
);
};
return MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilderWithDefaultWidget:
_bottomRowBuilderWithDefaultWidget,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
);
}),
),
),
),
@@ -4,6 +4,18 @@ import 'package:stream_chat_flutter/src/message_widget/message_widget_content_co
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message].
typedef BottomRowBuilder = Widget Function(BuildContext, Message);
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message] and the default [BottomRow].
typedef BottomRowBuilderWithDefaultWidget = Widget Function(
BuildContext,
Message,
BottomRow,
);
/// {@template messageWidgetContent}
/// The main content of a [StreamMessageWidget].
///
@@ -51,12 +63,28 @@ class MessageWidgetContent extends StatelessWidget {
this.onMentionTap,
this.onLinkTap,
this.textBuilder,
this.bottomRowBuilder,
this.onThreadTap,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.onThreadTap,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.userAvatarBuilder,
this.usernameBuilder,
});
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
/// {@macro reverse}
final bool reverse;
@@ -152,7 +180,10 @@ class MessageWidgetContent extends StatelessWidget {
final double bottomRowPadding;
/// {@macro bottomRowBuilder}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@macro bottomRowBuilderWithDefaultWidget}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@macro showInChannelIndicator}
final bool showInChannel;
@@ -207,30 +238,7 @@ class MessageWidgetContent extends StatelessWidget {
right: reverse ? bottomRowPadding : 0,
bottom: isPinned && showPinHighlight ? 6.0 : 0.0,
),
child: bottomRowBuilder?.call(
context,
message,
) ??
BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
),
child: _buildBottomRow(context),
),
Padding(
padding: EdgeInsets.only(
@@ -457,4 +465,39 @@ class MessageWidgetContent extends StatelessWidget {
),
);
}
Widget _buildBottomRow(BuildContext context) {
final defaultWidget = BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
);
if (bottomRowBuilder != null) {
return bottomRowBuilder!(context, message);
} else if (bottomRowBuilderWithDefaultWidget != null) {
return bottomRowBuilderWithDefaultWidget!(
context,
message,
defaultWidget,
);
}
return defaultWidget;
}
}
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template streamMessageReactionsModal}
@@ -39,35 +40,13 @@ class StreamMessageReactionsModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).currentUser;
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
final orientation = MediaQuery.of(context).orientation;
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final roughMaxSize = size.width * 2 / 3;
var messageTextLength = message.text!.length;
if (message.quotedMessage != null) {
var quotedMessageLength = message.quotedMessage!.text!.length + 40;
if (message.quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize = messageTextLength *
(messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2;
final divFactor = message.attachments.isNotEmpty
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final numberOfReactions =
StreamChatConfiguration.of(context).reactionIcons.length;
final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
final fontSize = messageTheme.messageTextStyle?.fontSize;
final child = Center(
child: SingleChildScrollView(
@@ -79,22 +58,26 @@ class StreamMessageReactionsModal extends StatelessWidget {
children: <Widget>[
if ((showReactions ?? hasReactionPermission) &&
(message.status == MessageSendingStatus.sent))
Align(
alignment: Alignment(
user!.id == message.user!.id
? (divFactor >= 1.0
? -0.2 - shiftFactor
: (1.2 - divFactor))
: (divFactor >= 1.0
? shiftFactor + 0.2
: -(1.2 - divFactor)),
0,
),
child: StreamReactionPicker(
message: message,
),
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
message,
constraints,
fontSize,
orientation,
),
0,
),
child: StreamReactionPicker(
message: message,
),
);
},
),
const SizedBox(height: 8),
const SizedBox(height: 10),
IgnorePointer(
child: messageWidget,
),
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// This method calculates the align that the modal of reactions should have.
/// This is an approximation based on the size of the message and the
/// available space in the screen.
double calculateReactionsHorizontalAlignment(
User? user,
Message message,
BoxConstraints constraints,
double? fontSize,
Orientation orientation,
) {
final maxWidth = constraints.maxWidth;
final roughSentenceSize = message.roughMessageSize(fontSize);
final hasAttachments = message.attachments.isNotEmpty;
final isReply = message.quotedMessageId != null;
final isAttachment = hasAttachments && !isReply;
// divFactor is the percentage of the available space that the message takes.
// When the divFactor is bigger than 0.5 that means that the messages is
// bigger than 50% of the available space and the modal should have an offset
// in the direction that the message grows. When the divFactor is smaller
// than 0.5 then the offset should be to he side opposite of the message
// growth.
// In resume, when divFactor > 0.5 then result > 0, when divFactor < 0.5
// then result < 0.
var divFactor = 0.5;
// When in portrait, attachments normally take 75% of the screen, when in
// landscape, attachments normally take 50% of the screen.
if (isAttachment) {
if (orientation == Orientation.portrait) {
divFactor = 0.75;
} else {
divFactor = 0.5;
}
} else {
divFactor = roughSentenceSize == 0 ? 0.5 : (roughSentenceSize / maxWidth);
}
final signal = user?.id == message.user?.id ? 1 : -1;
final result = signal * (1 - divFactor * 2.0);
return _capResult(result);
}
// Ensure reactions don't get pushed past the edge of the screen.
//
// This happens if divFactor is really big. When this happens, we can simply
// move the model all the way to the end of screen.
double _capResult(double result) {
if (result > 1.0) {
return 1;
} else if (result < -1.0) {
return -1;
} else {
return result;
}
}
@@ -1088,7 +1088,12 @@ class StreamSvgIcon extends StatelessWidget {
key: key,
width: width,
height: height,
color: color,
colorFilter: color != null
? ColorFilter.mode(
color!,
BlendMode.srcIn,
)
: null,
);
}
}
@@ -149,8 +149,8 @@ class StreamThreadHeader extends StatelessWidget
final theme = Theme.of(context);
return AppBar(
automaticallyImplyLeading: false,
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -409,7 +409,7 @@ class StreamChannelListErrorWidget extends StatelessWidget {
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: onPressed,
@@ -122,7 +122,8 @@ class StreamMessageSearchListTile extends StatelessWidget {
final title = this.title ??
MessageSearchListTileTitle(
messageResponse: messageResponse,
textStyle: channelPreviewTheme.titleStyle,
textStyle: channelPreviewTheme.titleStyle
?.copyWith(overflow: TextOverflow.ellipsis),
);
final subtitle = this.subtitle ??
@@ -177,27 +178,28 @@ class MessageSearchListTileTitle extends StatelessWidget {
final channel = messageResponse.channel;
final channelName = channel?.extraData['name'];
return Row(
children: [
Text(
user.id == StreamChat.of(context).currentUser?.id
? context.translations.youText
: user.name,
style: textStyle,
),
if (channelName != null) ...[
Text(
' ${context.translations.inText} ',
style: textStyle?.copyWith(
fontWeight: FontWeight.normal,
return Text.rich(
TextSpan(
children: [
TextSpan(
text: user.id == StreamChat.of(context).currentUser?.id
? context.translations.youText
: user.name,
),
if (channelName != null) ...[
TextSpan(
text: ' ${context.translations.inText} ',
style: textStyle?.copyWith(
fontWeight: FontWeight.normal,
),
),
),
Text(
channelName as String,
style: textStyle,
),
TextSpan(
text: channelName as String,
),
],
],
],
),
style: textStyle,
);
}
}
@@ -366,7 +366,7 @@ class StreamPhotoGallery extends StatelessWidget {
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
context.translations.noPhotoOrVideoLabel,
style: chatThemeData.textTheme.headline,
),
),
@@ -377,7 +377,7 @@ class StreamPhotoGallery extends StatelessWidget {
return StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
context.translations.genericErrorText,
textAlign: TextAlign.center,
),
);
@@ -400,7 +400,7 @@ class StreamPhotoGallery extends StatelessWidget {
return errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
errorTitle: Text(context.translations.genericErrorText),
onRetryPressed: controller.refresh,
),
);
@@ -21,6 +21,7 @@ class StreamMessageThemeData with Diagnosticable {
this.avatarTheme,
this.createdAtStyle,
this.linkBackgroundColor,
this.urlAttachmentTitleMaxLine,
});
/// Text style for message text
@@ -59,6 +60,9 @@ class StreamMessageThemeData with Diagnosticable {
/// Background color for messages with url attachments.
final Color? linkBackgroundColor;
/// Max number of lines in Url link title
final int? urlAttachmentTitleMaxLine;
/// Copy with a theme
StreamMessageThemeData copyWith({
TextStyle? messageTextStyle,
@@ -73,6 +77,7 @@ class StreamMessageThemeData with Diagnosticable {
Color? reactionsBorderColor,
Color? reactionsMaskColor,
Color? linkBackgroundColor,
int? urlAttachmentTitleMaxLine,
}) {
return StreamMessageThemeData(
messageTextStyle: messageTextStyle ?? this.messageTextStyle,
@@ -89,6 +94,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
urlAttachmentTitleMaxLine:
urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine,
);
}
@@ -148,6 +155,7 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor: other.reactionsBorderColor,
reactionsMaskColor: other.reactionsMaskColor,
linkBackgroundColor: other.linkBackgroundColor,
urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine,
);
}
@@ -167,7 +175,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor == other.reactionsBorderColor &&
reactionsMaskColor == other.reactionsMaskColor &&
avatarTheme == other.avatarTheme &&
linkBackgroundColor == other.linkBackgroundColor;
linkBackgroundColor == other.linkBackgroundColor &&
urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine;
@override
int get hashCode =>
@@ -182,7 +191,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor.hashCode ^
reactionsMaskColor.hashCode ^
avatarTheme.hashCode ^
linkBackgroundColor.hashCode;
linkBackgroundColor.hashCode ^
urlAttachmentTitleMaxLine.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -199,6 +209,10 @@ class StreamMessageThemeData with Diagnosticable {
..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor))
..add(ColorProperty('reactionsBorderColor', reactionsBorderColor))
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor))
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor));
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor))
..add(DiagnosticsProperty(
'urlAttachmentTitleMaxLine',
urlAttachmentTitleMaxLine,
));
}
}
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
@@ -12,6 +14,16 @@ extension StringExtension on String {
String capitalize() =>
isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
/// Returns the biggest line of a text.
String biggestLine() {
if (contains('\n')) {
return split('\n')
.reduce((curr, next) => curr.length > next.length ? curr : next);
} else {
return this;
}
}
/// Returns whether the string contains only emoji's or not.
///
/// Emojis guidelines
@@ -353,6 +365,33 @@ extension MessageX on Message {
return copyWith(text: messageTextToRender);
}
/// Returns an approximation of message size
double roughMessageSize(double? fontSize) {
var messageTextLength = min(text!.biggestLine().length, 65);
if (quotedMessage != null) {
var quotedMessageLength =
(min(quotedMessage!.text?.biggestLine().length ?? 0, 65)) + 8;
if (quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 8;
}
if (quotedMessageLength > messageTextLength * 1.2) {
messageTextLength = quotedMessageLength;
}
}
// Quoted message have a smaller font, so it is necessary to reduce the
// size of the multiplier to count for the smaller font.
var multiplier = 0.55;
if (quotedMessage != null) {
multiplier = 0.45;
}
return messageTextLength * (fontSize ?? 1) * multiplier;
}
/// It returns the message with the translated text if available locally
Message translate(String language) =>
copyWith(text: i18n?['${language}_text'] ?? text);
@@ -21,6 +21,9 @@ class _IVideoService {
///
/// Thumbnails are not supported on Web at this time.
///
/// If no [video] path is supplied, or if a thumbnail cannot be generated,
/// returns [generatePlaceholderThumbnail]. A stock placeholder image.
///
/// For desktop, you can specify the position of the video to generate
/// the thumbnail.
///
@@ -29,14 +32,14 @@ class _IVideoService {
/// creates lower quality of the thumbnail image, but it gets ignored for
/// PNG format.
Future<Uint8List?> generateVideoThumbnail({
required String video,
String? video,
ImageFormat imageFormat = ImageFormat.PNG,
int maxHeight = 0,
int maxWidth = 0,
int timeMs = 0,
int quality = 10,
}) async {
if (kIsWeb) {
if (kIsWeb || video == null) {
final placeholder = await generatePlaceholderThumbnail();
return placeholder;
}
@@ -8,13 +8,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
/// {@template streamVideoThumbnailImage}
/// Displays a video thumbnail for video attachments in a message.
/// Displays a video thumbnail for video attachments.
///
/// [thumbUrl] is used if provided.
///
/// Else [video] (path to local or remote video) is used to generate
/// a thumbnail from the video asset.
///
/// WARNING! a local path does not work on web.
///
/// If both [thumbUrl] and [video] are null, or if a thumbnail can't be
/// generated, a stock default image will be used.
/// {@endtemplate}
class StreamVideoThumbnailImage extends StatefulWidget {
/// {@macro streamVideoThumbnailImage}
const StreamVideoThumbnailImage({
super.key,
required this.video,
this.video,
this.thumbUrl,
this.constraints,
this.fit = BoxFit.cover,
@@ -24,7 +34,7 @@ class StreamVideoThumbnailImage extends StatefulWidget {
});
/// Video path or url
final String video;
final String? video;
/// Video thumbnail url
final String? thumbUrl;
@@ -1,6 +1,6 @@
import 'package:stream_chat_flutter/src/video/vlc/vlc_stub.dart'
if (dart.library.io) 'vlc_manager_desktop.dart'
if (dar.library.html) 'vlc_manager_web.dart';
if (dart.library.html) 'vlc_manager_web.dart';
/// {@template vlcManager}
/// An abstract class for the purpose of ensuring Flutter applications that
@@ -83,8 +83,6 @@ export 'src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart';
export 'src/scroll_view/stream_scroll_view_empty_widget.dart';
export 'src/scroll_view/stream_scroll_view_indexed_widget_builder.dart';
export 'src/scroll_view/user_scroll_view/stream_user_grid_tile.dart';
export 'src/scroll_view/user_scroll_view/stream_user_grid_tile.dart';
export 'src/scroll_view/user_scroll_view/stream_user_grid_view.dart';
export 'src/scroll_view/user_scroll_view/stream_user_grid_view.dart';
export 'src/scroll_view/user_scroll_view/stream_user_list_tile.dart';
export 'src/scroll_view/user_scroll_view/stream_user_list_view.dart';
+17 -19
View File
@@ -1,50 +1,48 @@
name: 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.
version: 5.1.0
version: 5.3.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment:
sdk: '>=2.17.0 <3.0.0'
sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.17.0"
dependencies:
cached_network_image: ^3.0.0
chewie: ^1.3.4
collection: ^1.15.0
collection: ^1.17.0
contextmenu: ^3.0.0
dart_vlc: ^0.3.0
desktop_drop: ^0.3.3
dart_vlc: ^0.4.0
desktop_drop: ^0.4.0
diacritic: ^0.1.3
dio: ^4.0.6
ezanimation: ^0.6.0
file_picker: ^4.1.3
file_picker: ^5.2.4
file_selector: ^0.9.0
flutter:
sdk: flutter
flutter_markdown: ^0.6.1
flutter_portal: ^1.0.0
flutter_svg: ^1.0.1
http: ^0.13.4
flutter_svg: ^2.0.4
http_parser: ^4.0.0
image_gallery_saver: ^1.7.0
image_gallery_saver: ^1.7.1
image_picker: ^0.8.2
jiffy: ^5.0.0
lottie: ^1.0.1
meta: ^1.3.0
lottie: ^2.0.0
meta: ^1.8.0
path_provider: ^2.0.9
photo_manager: ^2.0.1
photo_manager: ^2.5.2
photo_view: ^0.14.0
rxdart: ^0.27.0
share_plus: ^4.5.0
share_plus: ^6.3.0
shimmer: ^2.0.0
stream_chat_flutter_core: ^5.1.0
stream_chat_flutter_core: ^5.3.0
synchronized: ^3.0.0
thumblr: ^0.0.4
url_launcher: ^6.1.0
video_player: ^2.4.5
video_player_macos: ^1.0.6
video_player_macos: ^2.0.1
video_thumbnail: ^0.5.0
flutter:
@@ -56,9 +54,9 @@ flutter:
uses-material-design: true
dev_dependencies:
dart_code_metrics: ^4.4.0
dart_code_metrics: ^5.7.0
flutter_test:
sdk: flutter
golden_toolkit: ^0.13.0
golden_toolkit: ^0.15.0
mocktail: ^0.3.0
path: ^1.8.0
path: ^1.8.2
@@ -163,7 +163,7 @@ void main() {
);
testWidgets(
'tapping on reply should pop',
'tapping on reply should invoke callback',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
@@ -174,7 +174,7 @@ void main() {
final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(themeData);
final mockObserver = MockNavigatorObserver();
final mockCallback = MockVoidCallback();
final attachment = Attachment(
type: 'image',
@@ -192,7 +192,6 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: themeData,
navigatorObservers: [mockObserver],
home: StreamChat(
streamChatThemeData: streamTheme,
client: client,
@@ -200,13 +199,14 @@ void main() {
child: AttachmentActionsModal(
message: message,
attachment: attachment,
onReply: mockCallback,
),
),
),
),
);
await tester.tap(find.text('Reply'));
verify(() => mockObserver.didPop(any(), any()));
verify(mockCallback.call);
},
);
@@ -1,3 +1,17 @@
## Upcoming
- Updated dependencies to resolvable versions.
## 5.3.0
- Updated `stream_chat` dependency to [`5.3.0`](https://pub.dev/packages/stream_chat/changelog).
## 5.2.0
🔄 Changed
- Updated `connectivity_plus` dependency to `^3.0.2`
## 5.1.0
- Deprecated the `sort` parameter in the `StreamChannelListController` in favor of `channelStateSort`.
@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 29
compileSdkVersion 33
lintOptions {
disable 'InvalidPackage'
@@ -35,7 +35,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "example.example"
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -7,14 +7,15 @@
FlutterApplication and put your custom class here. -->
<application
android:name="${applicationName}"
android:label="example"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user

Some files were not shown because too many files have changed in this diff Show More