Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into fix/indicator

 Conflicts:
	packages/stream_chat/CHANGELOG.md
This commit is contained in:
Deven Joshi
2021-11-22 14:27:48 +05:30
13 changed files with 152 additions and 29 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
flutter-version: ${{ env.flutter_version }} flutter-version: ${{ env.flutter_version }}
- name: "Install Tools" - name: "Install Tools"
run: flutter pub global activate melos 1.0.0-dev.6 run: flutter pub global activate melos 1.0.0-dev.10
- name: "Bootstrap Workspace" - name: "Bootstrap Workspace"
run: melos bootstrap run: melos bootstrap
@@ -31,7 +31,7 @@ jobs:
flutter-version: ${{ env.flutter_version }} flutter-version: ${{ env.flutter_version }}
- name: "Install Tools" - name: "Install Tools"
run: | run: |
flutter pub global activate melos 1.0.0-dev.6 flutter pub global activate melos 1.0.0-dev.10
- name: "Bootstrap Workspace" - name: "Bootstrap Workspace"
run: melos bootstrap run: melos bootstrap
- name: "Dart Analyze" - name: "Dart Analyze"
+8 -1
View File
@@ -2,8 +2,15 @@
🐞 Fixed 🐞 Fixed
- `closeConnection()` now uses `normalClosure` status when closing websocket.
- Fixed unread count indicator - Fixed unread count indicator
## Upcoming
✅ Added
- Extra properties added to `PaginationParams` to aid in fetching messages.
## 3.2.0 ## 3.2.0
🐞 Fixed 🐞 Fixed
@@ -726,4 +733,4 @@
## 0.0.2 ## 0.0.2
- first beta version - first beta version
@@ -60,8 +60,11 @@ class PaginationParams extends Equatable {
/// ``` /// ```
const PaginationParams({ const PaginationParams({
this.limit = 10, this.limit = 10,
this.before = 10,
this.after = 10,
this.offset, this.offset,
this.next, this.next,
this.idAround,
this.greaterThan, this.greaterThan,
this.greaterThanOrEqual, this.greaterThanOrEqual,
this.lessThan, this.lessThan,
@@ -78,12 +81,22 @@ class PaginationParams extends Equatable {
/// The amount of items requested from the APIs. /// The amount of items requested from the APIs.
final int limit; final int limit;
/// The amount of items requested before message ID from the APIs.
final int before;
/// The amount of items requested after message ID from the APIs.
final int after;
/// The offset of requesting items. /// The offset of requesting items.
final int? offset; final int? offset;
/// A key used to paginate. /// A key used to paginate.
final String? next; final String? next;
/// Message ID to fetch messages around
@JsonKey(name: 'id_around')
final String? idAround;
/// Filter on ids greater than the given value. /// Filter on ids greater than the given value.
@JsonKey(name: 'id_gt') @JsonKey(name: 'id_gt')
final String? greaterThan; final String? greaterThan;
@@ -106,7 +119,10 @@ class PaginationParams extends Equatable {
/// Creates a copy of [PaginationParams] with specified attributes overridden. /// Creates a copy of [PaginationParams] with specified attributes overridden.
PaginationParams copyWith({ PaginationParams copyWith({
int? limit, int? limit,
int? before,
int? after,
int? offset, int? offset,
String? idAround,
String? next, String? next,
String? greaterThan, String? greaterThan,
String? greaterThanOrEqual, String? greaterThanOrEqual,
@@ -115,7 +131,10 @@ class PaginationParams extends Equatable {
}) => }) =>
PaginationParams( PaginationParams(
limit: limit ?? this.limit, limit: limit ?? this.limit,
before: before ?? this.before,
after: limit ?? this.after,
offset: offset ?? this.offset, offset: offset ?? this.offset,
idAround: idAround ?? this.idAround,
next: next ?? this.next, next: next ?? this.next,
greaterThan: greaterThan ?? this.greaterThan, greaterThan: greaterThan ?? this.greaterThan,
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
@@ -126,8 +145,11 @@ class PaginationParams extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
limit, limit,
before,
after,
offset, offset,
next, next,
idAround,
greaterThan, greaterThan,
greaterThanOrEqual, greaterThanOrEqual,
lessThan, lessThan,
@@ -21,8 +21,11 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) => PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
PaginationParams( PaginationParams(
limit: json['limit'] as int? ?? 10, limit: json['limit'] as int? ?? 10,
before: json['before'] as int? ?? 10,
after: json['after'] as int? ?? 10,
offset: json['offset'] as int?, offset: json['offset'] as int?,
next: json['next'] as String?, next: json['next'] as String?,
idAround: json['id_around'] as String?,
greaterThan: json['id_gt'] as String?, greaterThan: json['id_gt'] as String?,
greaterThanOrEqual: json['id_gte'] as String?, greaterThanOrEqual: json['id_gte'] as String?,
lessThan: json['id_lt'] as String?, lessThan: json['id_lt'] as String?,
@@ -32,6 +35,8 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) { Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
'limit': instance.limit, 'limit': instance.limit,
'before': instance.before,
'after': instance.after,
}; };
void writeNotNull(String key, dynamic value) { void writeNotNull(String key, dynamic value) {
@@ -42,6 +47,7 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
writeNotNull('offset', instance.offset); writeNotNull('offset', instance.offset);
writeNotNull('next', instance.next); writeNotNull('next', instance.next);
writeNotNull('id_around', instance.idAround);
writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan); writeNotNull('id_lt', instance.lessThan);
@@ -121,7 +121,8 @@ class WebSocket with TimerHelper {
_logger?.info('Closing connection with $baseUrl'); _logger?.info('Closing connection with $baseUrl');
if (_webSocketChannel != null) { if (_webSocketChannel != null) {
_unsubscribeFromWebSocketChannel(); _unsubscribeFromWebSocketChannel();
_webSocketChannel?.sink.close(status.goingAway); _webSocketChannel?.sink
.close(_manuallyClosed ? status.normalClosure : status.goingAway);
_webSocketChannel = null; _webSocketChannel = null;
} }
} }
+8 -1
View File
@@ -1,8 +1,15 @@
## Upcoming ## Upcoming
✅ Added
- `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`.
- `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call.
🐞 Fixed 🐞 Fixed
- [[#766]]`AttachmentActionsModal` now has customisation options for actions. - [[#766]]`AttachmentActionsModal` now has customisation options for actions.
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
- Fixed adding attachments on web.
## 3.2.0 ## 3.2.0
@@ -866,4 +873,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
## 0.0.1 ## 0.0.1
- First release - First release
@@ -1,6 +1,7 @@
import 'package:characters/characters.dart'; import 'package:characters/characters.dart';
import 'package:diacritic/diacritic.dart'; import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/localization/translations.dart'; import 'package:stream_chat_flutter/src/localization/translations.dart';
@@ -46,7 +47,7 @@ extension IterableX<T> on Iterable<T> {
extension PlatformFileX on PlatformFile { extension PlatformFileX on PlatformFile {
/// Converts the [PlatformFile] into [AttachmentFile] /// Converts the [PlatformFile] into [AttachmentFile]
AttachmentFile get toAttachmentFile => AttachmentFile( AttachmentFile get toAttachmentFile => AttachmentFile(
path: path, path: kIsWeb ? null : path,
name: name, name: name,
bytes: bytes, bytes: bytes,
size: size, size: size,
@@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message);
/// Callback on reply tapped /// Callback on reply tapped
typedef ReplyTapCallback = void Function(Message); typedef ReplyTapCallback = void Function(Message);
/// Spacing Types (These are properties of a message to help inform the decision
/// of how much space / which widget to build after it)
enum SpacingType {
/// Message is a thread
thread,
/// There is a >1s time diff between current and last message
timeDiff,
/// Next message is by a different user
otherUser,
/// Message is deleted
deleted,
/// No other conditions are valid, default spacing (This will likely be the
/// only rule in the list provided)
defaultSpacing,
}
/// Builder for building certain spacing after widgets.
/// This spacing can be in form of any widgets you like.
/// A List of [SpacingType] is provided to help inform the decision of
/// what to build after the message.
///
/// As an example:
/// MessageListView(
/// spacingWidgetBuilder: (context, list) {
/// if(list.contains(SpacingType.defaultSpacing)) {
/// return SizedBox(height: 2.0,);
/// } else {
/// return SizedBox(height: 8.0,);
/// }
/// },
/// ),
typedef SpacingWidgetBuilder = Widget Function(
BuildContext context,
List<SpacingType> spacingTypes,
);
/// Class for message details /// Class for message details
// ignore: prefer-match-file-name // ignore: prefer-match-file-name
class MessageDetails { class MessageDetails {
@@ -171,6 +211,7 @@ class MessageListView extends StatefulWidget {
this.reverse = true, this.reverse = true,
this.paginationLimit = 20, this.paginationLimit = 20,
this.paginationLoadingIndicatorBuilder, this.paginationLoadingIndicatorBuilder,
this.spacingWidgetBuilder,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -289,6 +330,12 @@ class MessageListView extends StatefulWidget {
/// Builder used to build the loading indicator shown while paginating. /// Builder used to build the loading indicator shown while paginating.
final WidgetBuilder? paginationLoadingIndicatorBuilder; final WidgetBuilder? paginationLoadingIndicatorBuilder;
/// This allows a user to customise the space after a message
/// A List of [SpacingType] is provided to provide more data about the
/// type of message (thread, difference in time between current and last
/// message, default spacing, etc)
final SpacingWidgetBuilder? spacingWidgetBuilder;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -564,17 +611,38 @@ class _MessageListViewState extends State<MessageListView> {
Units.MINUTE, Units.MINUTE,
); );
final spacingRules = <SpacingType>[];
final isNextUserSame = final isNextUserSame =
message.user!.id == nextMessage.user?.id; message.user!.id == nextMessage.user?.id;
final isThread = message.replyCount! > 0; final isThread = message.replyCount! > 0;
final isDeleted = message.isDeleted; final isDeleted = message.isDeleted;
if (timeDiff >= 1 || final hasTimeDiff = timeDiff >= 1;
!isNextUserSame ||
isThread || if (hasTimeDiff) {
isDeleted) { spacingRules.add(SpacingType.timeDiff);
return const SizedBox(height: 8);
} }
return const SizedBox(height: 2);
if (!isNextUserSame) {
spacingRules.add(SpacingType.otherUser);
}
if (isThread) {
spacingRules.add(SpacingType.thread);
}
if (isDeleted) {
spacingRules.add(SpacingType.deleted);
}
if (spacingRules.isNotEmpty) {
return widget.spacingWidgetBuilder
?.call(context, spacingRules) ??
const SizedBox(height: 8);
}
return widget.spacingWidgetBuilder
?.call(context, [SpacingType.defaultSpacing]) ??
const SizedBox(height: 2);
}, },
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == itemCount - 1) { if (i == itemCount - 1) {
@@ -1230,6 +1230,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildSendingIndicator() { Widget _buildSendingIndicator() {
final style = widget.messageTheme.createdAtStyle; final style = widget.messageTheme.createdAtStyle;
final message = widget.message; final message = widget.message;
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
if (hasNonUrlAttachments && if (hasNonUrlAttachments &&
(message.status == MessageSendingStatus.sending || (message.status == MessageSendingStatus.sending ||
@@ -1260,7 +1261,7 @@ class _MessageWidgetState extends State<MessageWidget>
if (isMessageRead) { if (isMessageRead) {
child = Row( child = Row(
children: [ children: [
if (StreamChannel.of(context).channel.memberCount! > 2) if (memberCount > 2)
Text( Text(
widget.readList!.length.toString(), widget.readList!.length.toString(),
style: style.copyWith( style: style.copyWith(
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart'; export 'src/attachment/attachment.dart';
export 'src/attachment_actions_modal.dart';
export 'src/back_button.dart'; export 'src/back_button.dart';
export 'src/channel_avatar.dart'; export 'src/channel_avatar.dart';
export 'src/channel_header.dart'; export 'src/channel_header.dart';
@@ -227,13 +227,13 @@ class StreamChannelState extends State<StreamChannel> {
preferOffline: preferOffline, preferOffline: preferOffline,
); );
Future<List<ChannelState>> _queryAtMessage({ Future<ChannelState?> _queryAtMessage({
String? messageId, String? messageId,
int before = 20, int before = 20,
int after = 20, int after = 20,
bool preferOffline = false, bool preferOffline = false,
}) async { }) async {
if (channel.state == null) return []; if (channel.state == null) return null;
channel.state!.isUpToDate = false; channel.state!.isUpToDate = false;
channel.state!.truncate(); channel.state!.truncate();
@@ -245,23 +245,33 @@ class StreamChannelState extends State<StreamChannel> {
preferOffline: preferOffline, preferOffline: preferOffline,
); );
channel.state!.isUpToDate = true; channel.state!.isUpToDate = true;
return []; return null;
} }
return Future.wait([ return queryAroundMessage(
queryBeforeMessage( messageId,
messageId, before: before,
limit: before, after: after,
preferOffline: preferOffline, preferOffline: preferOffline,
), );
queryAfterMessage(
messageId,
limit: after,
preferOffline: preferOffline,
),
]);
} }
///
Future<ChannelState> queryAroundMessage(
String messageId, {
int before = 20,
int after = 20,
bool preferOffline = false,
}) =>
channel.query(
messagesPagination: PaginationParams(
idAround: messageId,
before: before,
after: after,
),
preferOffline: preferOffline,
);
/// ///
Future<ChannelState> queryBeforeMessage( Future<ChannelState> queryBeforeMessage(
String messageId, { String messageId, {