From fcb9e308c714ea3a6a4ca2f6570175d73c49dd0c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 17 Nov 2021 06:11:46 +0530 Subject: [PATCH 01/19] chore(ui): initial channel list view draft with controller. Signed-off-by: xsahil03x --- .../example/lib/tutorial_part_2.dart | 45 +- .../lib/src/channel_info.dart | 11 +- .../lib/src/group_avatar.dart | 5 +- .../lib/src/option_list_tile.dart | 17 +- .../lib/src/paged_value_notifier.dart | 92 +++ .../lib/src/paged_value_notifier.freezed.dart | 590 ++++++++++++++++++ .../lib/src/thread_header.dart | 13 +- .../lib/src/typing_indicator.dart | 41 +- .../stream_channel_list_controller.dart | 91 +++ .../stream_channel_list_loading_tile.dart | 91 +++ .../stream_channel_list_tile.dart | 379 +++++++++++ .../stream_channel_list_view.dart | 369 +++++++++++ .../lib/src/v4/stream_channel_avatar.dart | 201 ++++++ .../lib/src/v4/stream_channel_name.dart | 93 +++ .../lib/stream_chat_flutter.dart | 2 + 15 files changed, 1983 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/paged_value_notifier.dart create mode 100644 packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6abc260e..6e1edaba 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -62,29 +62,52 @@ class MyApp extends StatelessWidget { client: client, child: child, ), - home: const ChannelListPage(), + home: ChannelListPage( + client: client, + ), ); } } -class ChannelListPage extends StatelessWidget { - const ChannelListPage({ +class ChannelListPage extends StatefulWidget { + ChannelListPage({ Key? key, + required this.client, }) : super(key: key); + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 62bfd1de..0c4f4ac7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -94,11 +94,12 @@ class ChannelInfo extends StatelessWidget { return alternativeWidget ?? const Offstage(); } - return TypingIndicator( - parentId: parentId, - alignment: Alignment.center, - alternativeWidget: alternativeWidget, - style: textStyle, + return Align( + child: TypingIndicator( + parentId: parentId, + style: textStyle, + alternativeWidget: alternativeWidget, + ), ); } diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index bf599c63..d7996c4d 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -6,6 +6,7 @@ class GroupAvatar extends StatelessWidget { /// Constructor for creating a [GroupAvatar] const GroupAvatar({ Key? key, + this.channel, required this.members, this.constraints, this.onTap, @@ -15,6 +16,8 @@ class GroupAvatar extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + final Channel? channel; + /// List of images to display final List members; @@ -38,7 +41,7 @@ class GroupAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; + final channel = this.channel ?? StreamChannel.of(context).channel; assert(channel.state != null, 'Channel ${channel.id} is not initialized'); diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 16bf9e97..b8daa0e3 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -6,7 +6,7 @@ class OptionListTile extends StatelessWidget { /// Constructor for creating [OptionListTile] const OptionListTile({ Key? key, - this.title, + required this.title, this.leading, this.trailing, this.onTap, @@ -17,7 +17,7 @@ class OptionListTile extends StatelessWidget { }) : super(key: key); /// Title for tile - final String? title; + final String title; /// Leading widget (start) final Widget? leading; @@ -46,8 +46,8 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: separatorColor ?? chatThemeData.colorTheme.disabled, height: 1, + color: separatorColor ?? chatThemeData.colorTheme.disabled, ), Material( color: tileColor ?? chatThemeData.colorTheme.barsBg, @@ -57,15 +57,14 @@ class OptionListTile extends StatelessWidget { onTap: onTap, child: Row( children: [ - if (leading != null) Center(child: leading), - if (leading == null) - const SizedBox( - width: 16, - ), + if (leading != null) + Center(child: leading) + else + const SizedBox(width: 16), Expanded( flex: 4, child: Text( - title!, + title, style: titleTextStyle ?? (titleColor == null ? chatThemeData.textTheme.bodyBold diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart new file mode 100644 index 00000000..d71db24d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/stream_chat.dart' show StreamChatError; + +part 'paged_value_notifier.freezed.dart'; + +const defaultInitialPagedLimitMultiplier = 3; + +typedef PagedValueListenableBuilder + = ValueListenableBuilder>; + +abstract class PagedValueNotifier + extends ValueNotifier> { + /// Creates a [PagedValueNotifier] + PagedValueNotifier(this._initialValue) : super(_initialValue); + + /// Stores initialValue in case we need to call [refresh]. + final PagedValue _initialValue; + + /// Retry any failed load requests. + /// + /// Unlike [refresh], this does not resets the whole [value], + /// it only retries the last failed load request. + Future retry() { + var lastValue = value; + assert(lastValue.hasError, ''); + lastValue = lastValue as Success; + + final nextPageKey = lastValue.nextPageKey; + // resetting the error + value = lastValue.copyWith(error: null); + return loadMore(nextPageKey!); + } + + /// Refresh the data presented by this [PagedValueNotifier]. + /// + /// Note: This API is intended for UI-driven refresh signals, + /// such as swipe-to-refresh. + Future refresh() { + value = _initialValue; + return doInitialLoad(); + } + + /// Load initial data from the server. + Future doInitialLoad(); + + /// Load more data from the server using [nextPageKey]. + Future loadMore(Key nextPageKey); +} + +@freezed +abstract class PagedValue with _$PagedValue { + const PagedValue._(); + + /// Creates a new instance of [PagedValue] with the given [key] and [value]. + // @Assert( + // 'nextPageKey != null', + // 'Cannot set an error if all the pages are already fetched', + // ) + const factory PagedValue({ + /// List with all items loaded so far. + required List items, + + /// The key for the next page to be fetched. + Key? nextPageKey, + + /// The current error, if any. + StreamChatError? error, + }) = Success; + + bool get hasNextPage { + assert(this is Success, ''); + return (this as Success).nextPageKey != null; + } + + bool get hasError { + assert(this is Success, ''); + return (this as Success).error != null; + } + + int get itemCount { + assert(this is Success, ''); + final count = (this as Success).items.length; + if (hasNextPage || hasError) return count + 1; + return count; + } + + const factory PagedValue.loading() = Loading; + + const factory PagedValue.error(StreamChatError error) = Error; +} diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart new file mode 100644 index 00000000..515a9742 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart @@ -0,0 +1,590 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// 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 + +part of 'paged_value_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +class _$PagedValueTearOff { + const _$PagedValueTearOff(); + + Success call( + {required List items, Key? nextPageKey, StreamChatError? error}) { + return Success( + items: items, + nextPageKey: nextPageKey, + error: error, + ); + } + + Loading loading() { + return Loading(); + } + + Error error(StreamChatError error) { + return Error( + error, + ); + } +} + +/// @nodoc +const $PagedValue = _$PagedValueTearOff(); + +/// @nodoc +mixin _$PagedValue { + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PagedValueCopyWith { + factory $PagedValueCopyWith(PagedValue value, + $Res Function(PagedValue) then) = + _$PagedValueCopyWithImpl; +} + +/// @nodoc +class _$PagedValueCopyWithImpl + implements $PagedValueCopyWith { + _$PagedValueCopyWithImpl(this._value, this._then); + + final PagedValue _value; + // ignore: unused_field + final $Res Function(PagedValue) _then; +} + +/// @nodoc +abstract class $SuccessCopyWith { + factory $SuccessCopyWith( + Success value, $Res Function(Success) then) = + _$SuccessCopyWithImpl; + $Res call({List items, Key? nextPageKey, StreamChatError? error}); +} + +/// @nodoc +class _$SuccessCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $SuccessCopyWith { + _$SuccessCopyWithImpl( + Success _value, $Res Function(Success) _then) + : super(_value, (v) => _then(v as Success)); + + @override + Success get _value => super._value as Success; + + @override + $Res call({ + Object? items = freezed, + Object? nextPageKey = freezed, + Object? error = freezed, + }) { + return _then(Success( + items: items == freezed + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + nextPageKey: nextPageKey == freezed + ? _value.nextPageKey + : nextPageKey // ignore: cast_nullable_to_non_nullable + as Key?, + error: error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError?, + )); + } +} + +/// @nodoc + +class _$Success extends Success + with DiagnosticableTreeMixin { + const _$Success({required this.items, this.nextPageKey, this.error}) + : super._(); + + @override + + /// List with all items loaded so far. + final List items; + @override + + /// The key for the next page to be fetched. + final Key? nextPageKey; + @override + + /// The current error, if any. + final StreamChatError? error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>(items: $items, nextPageKey: $nextPageKey, error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>')) + ..add(DiagnosticsProperty('items', items)) + ..add(DiagnosticsProperty('nextPageKey', nextPageKey)) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other is Success && + (identical(other.items, items) || + const DeepCollectionEquality().equals(other.items, items)) && + (identical(other.nextPageKey, nextPageKey) || + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey)) && + (identical(other.error, error) || + const DeepCollectionEquality().equals(other.error, error))); + } + + @override + int get hashCode => + runtimeType.hashCode ^ + const DeepCollectionEquality().hash(items) ^ + const DeepCollectionEquality().hash(nextPageKey) ^ + const DeepCollectionEquality().hash(error); + + @JsonKey(ignore: true) + @override + $SuccessCopyWith> get copyWith => + _$SuccessCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return $default(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return $default?.call(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(items, nextPageKey, this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class Success extends PagedValue { + const factory Success( + {required List items, + Key? nextPageKey, + StreamChatError? error}) = _$Success; + const Success._() : super._(); + + /// List with all items loaded so far. + List get items => throw _privateConstructorUsedError; + + /// The key for the next page to be fetched. + Key? get nextPageKey => throw _privateConstructorUsedError; + + /// The current error, if any. + StreamChatError? get error => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SuccessCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingCopyWith { + factory $LoadingCopyWith( + Loading value, $Res Function(Loading) then) = + _$LoadingCopyWithImpl; +} + +/// @nodoc +class _$LoadingCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $LoadingCopyWith { + _$LoadingCopyWithImpl( + Loading _value, $Res Function(Loading) _then) + : super(_value, (v) => _then(v as Loading)); + + @override + Loading get _value => super._value as Loading; +} + +/// @nodoc + +class _$Loading extends Loading + with DiagnosticableTreeMixin { + const _$Loading() : super._(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.loading()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || (other is Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class Loading extends PagedValue { + const factory Loading() = _$Loading; + const Loading._() : super._(); +} + +/// @nodoc +abstract class $ErrorCopyWith { + factory $ErrorCopyWith( + Error value, $Res Function(Error) then) = + _$ErrorCopyWithImpl; + $Res call({StreamChatError error}); +} + +/// @nodoc +class _$ErrorCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $ErrorCopyWith { + _$ErrorCopyWithImpl( + Error _value, $Res Function(Error) _then) + : super(_value, (v) => _then(v as Error)); + + @override + Error get _value => super._value as Error; + + @override + $Res call({ + Object? error = freezed, + }) { + return _then(Error( + error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError, + )); + } +} + +/// @nodoc + +class _$Error extends Error + with DiagnosticableTreeMixin { + const _$Error(this.error) : super._(); + + @override + final StreamChatError error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.error(error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.error')) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other is Error && + (identical(other.error, error) || + const DeepCollectionEquality().equals(other.error, error))); + } + + @override + int get hashCode => + runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); + + @JsonKey(ignore: true) + @override + $ErrorCopyWith> get copyWith => + _$ErrorCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return error(this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return error?.call(this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class Error extends PagedValue { + const factory Error(StreamChatError error) = _$Error; + const Error._() : super._(); + + StreamChatError get error => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ErrorCopyWith> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index dfe13b41..5f7fb25e 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -163,12 +163,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(height: 2), if (showTypingIndicator) - TypingIndicator( - alignment: Alignment.center, - channel: StreamChannel.of(context).channel, - style: channelHeaderTheme.subtitleStyle, - parentId: parent.id, - alternativeWidget: defaultSubtitle, + Align( + child: TypingIndicator( + channel: StreamChannel.of(context).channel, + style: channelHeaderTheme.subtitleStyle, + parentId: parent.id, + alternativeWidget: defaultSubtitle, + ), ) else defaultSubtitle, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index d961a8ff..389d1020 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -11,7 +11,6 @@ class TypingIndicator extends StatelessWidget { this.channel, this.alternativeWidget, this.style, - this.alignment = Alignment.centerLeft, this.padding = const EdgeInsets.all(0), this.parentId, }) : super(key: key); @@ -28,9 +27,6 @@ class TypingIndicator extends StatelessWidget { /// The padding of this widget final EdgeInsets padding; - /// Alignment of the typing indicator - final Alignment alignment; - /// Id of the parent message in case of a thread final String? parentId; @@ -46,30 +42,25 @@ class TypingIndicator extends StatelessWidget { stream: channelState.typingEventsStream.map((typings) => typings.entries .where((element) => element.value.parentId == parentId) .map((e) => e.key)), - builder: (context, data) => AnimatedSwitcher( + builder: (context, users) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: data.isNotEmpty + child: users.isNotEmpty ? Padding( - key: const Key('main'), padding: padding, - child: Align( - key: const Key('typings'), - alignment: alignment, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Lottie.asset( - 'animations/typing_dots.json', - package: 'stream_chat_flutter', - height: 4, - ), - Text( - context.translations.userTypingText(data), - maxLines: 1, - style: style, - ), - ], - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Lottie.asset( + 'animations/typing_dots.json', + package: 'stream_chat_flutter', + height: 4, + ), + Text( + context.translations.userTypingText(users), + maxLines: 1, + style: style, + ), + ], ), ) : altWidget, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart new file mode 100644 index 00000000..4bfc7130 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -0,0 +1,91 @@ +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; + +class StreamChannelListController extends PagedValueNotifier { + /// Creates a [StreamChannelListController]. + StreamChannelListController({ + required this.client, + this.filter, + this.sort, + this.limit = 2, + this.messageLimit, + this.memberLimit, + }) : super(const PagedValue.loading()); + + /// Creates a [StreamChannelListController] from the passed [value]. + StreamChannelListController.fromValue( + PagedValue value, { + required this.client, + this.filter, + this.sort, + this.limit = 2, + this.messageLimit, + this.memberLimit, + }) : super(value); + + /// The client to use for the channel list. + final StreamChatClient client; + + /// The filter to apply to the channel list. + final Filter? filter; + + /// The sort to apply to the channel list. + final List>? sort; + + /// The limit to apply to the channel list. + final int limit; + + /// The limit to apply to the message list. + final int? messageLimit; + + /// The limit to apply to the member list. + final int? memberLimit; + + @override + Future doInitialLoad() async { + final limit = this.limit * defaultInitialPagedLimitMultiplier; + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: PaginationParams(limit: limit), + )) { + final nextKey = channels.length < limit ? null : channels.length; + value = PagedValue( + items: channels, + nextPageKey: nextKey, + ); + } + } catch (error) { + value = PagedValue.error(StreamChatError('error')); + } + } + + @override + Future loadMore(int nextPageKey) async { + assert(value is Success, ''); + final previousValue = value as Success; + + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: PaginationParams(limit: limit, offset: nextPageKey), + )) { + final previousItems = previousValue.items; + final newItems = previousItems + channels; + final nextKey = channels.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } + } catch (error) { + value = previousValue.copyWith(error: StreamChatError('error')); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart new file mode 100644 index 00000000..9e0231d5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +class StreamChannelListLoadingTile extends StatelessWidget { + const StreamChannelListLoadingTile({ + Key? key, + this.visualDensity = VisualDensity.standard, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : super(key: key); + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + @override + Widget build(BuildContext context) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + + final leading = Container( + height: 49, + width: 49, + decoration: BoxDecoration( + color: colorTheme.barsBg, + shape: BoxShape.circle, + ), + ); + + final title = Container( + height: 16, + width: 66, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ); + + final subtitle = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + height: 16, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 16, + width: 50, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ); + + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + visualDensity: visualDensity, + contentPadding: contentPadding, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart new file mode 100644 index 00000000..f4fb4fde --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -0,0 +1,379 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart' show Channel; +import 'package:stream_chat_flutter/src/sending_indicator.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/channel_preview_theme.dart'; +import 'package:stream_chat_flutter/src/typing_indicator.dart'; +import 'package:stream_chat_flutter/src/unread_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; + +class StreamChannelListTile extends StatelessWidget { + StreamChannelListTile({ + Key? key, + required this.channel, + this.leading, + this.title, + this.subtitle, + this.onTap, + this.onLongPress, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// A widget to display before the title. + /// + /// Typically an [Icon] or a [CircleAvatar] widget. + final Widget? leading; + + /// The primary content of the list tile. + /// + /// Typically a [Text] widget. + /// + /// This should not wrap. To enforce the single line limit, use + /// [Text.maxLines]. + final Widget? title; + + /// Additional content displayed below the title. + /// + /// Typically a [Text] widget. + /// + /// If [isThreeLine] is false, this should not wrap. + /// + /// If [isThreeLine] is true, this should be configured to take a maximum of + /// two lines. For example, you can use [Text.maxLines] to enforce the number + /// of lines. + /// + /// The subtitle's default [TextStyle] depends on [TextTheme.bodyText2] except + /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled] + /// and [selected]. + /// + /// When [enabled] is false, the text color is set to [ThemeData.disabledColor]. + /// + /// When [selected] is false, the text color is set to [ListTileTheme.textColor] + /// if it's not null and to [TextTheme.caption]'s color if [ListTileTheme.textColor] + /// is null. + final Widget? subtitle; + + /// Called when the user taps this list tile. + /// + /// Inoperative if [enabled] is false. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + /// + /// Inoperative if [enabled] is false. + final GestureLongPressCallback? onLongPress; + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + @override + Widget build(BuildContext context) { + final channelState = channel.state!; + final currentUser = channel.client.state.currentUser!; + + final channelPreviewTheme = ChannelPreviewTheme.of(context); + + final leading = this.leading ?? + StreamChannelAvatar( + channel: channel, + ); + + final title = this.title ?? + StreamChannelName( + channel: channel, + textStyle: channelPreviewTheme.titleStyle, + ); + + final subtitle = this.subtitle ?? + ChannelListTileSubtitle( + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ); + + return BetterStreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, isMuted) => AnimatedOpacity( + opacity: isMuted ? 0.5 : 1, + duration: const Duration(milliseconds: 300), + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + visualDensity: visualDensity, + contentPadding: contentPadding, + leading: leading, + title: Row( + children: [ + Expanded(child: title), + BetterStreamBuilder>( + stream: channelState.membersStream, + initialData: channelState.members, + comparator: const ListEquality().equals, + builder: (context, members) { + if (members.isEmpty || + !members.any((it) => it.user!.id == currentUser.id)) { + return const Offstage(); + } + return UnreadIndicator(cid: channel.cid); + }, + ), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: subtitle, + ), + ), + BetterStreamBuilder>( + stream: channelState.messagesStream, + initialData: channelState.messages, + comparator: const ListEquality().equals, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null || + (lastMessage.user?.id != currentUser.id)) { + return const Offstage(); + } + + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState.read + .where((it) => it.user.id != currentUser.id) + .where( + (it) => it.lastRead.isAfter(lastMessage.createdAt), + ) + .isNotEmpty, + ), + ); + }, + ), + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ), + // trailing ?? _buildDate(context), + ], + ), + ), + ), + ); + } +} + +class ChannelLastMessageDate extends StatelessWidget { + ChannelLastMessageDate({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, data) { + final lastMessageAt = data.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = context.translations.yesterdayLabel; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: textStyle, + ); + }, + ); +} + +class ChannelListTileSubtitle extends StatelessWidget { + ChannelListTileSubtitle({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + if (channel.isMuted) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSvgIcon.mute(size: 16), + Text( + ' ${context.translations.channelIsMutedText}', + style: textStyle, + ), + ], + ); + } + return TypingIndicator( + channel: channel, + style: textStyle, + alternativeWidget: ChannelLastMessageText( + channel: channel, + textStyle: textStyle, + ), + ); + } +} + +class ChannelLastMessageText extends StatelessWidget { + ChannelLastMessageText({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null) return const Offstage(); + + final lastMessageText = lastMessage.text; + final lastMessageAttachments = lastMessage.attachments; + final lastMessageMentionedUsers = lastMessage.mentionedUsers; + + final messageTextParts = [ + ...lastMessageAttachments.map((it) { + if (it.type == 'image') { + return '📷'; + } else if (it.type == 'video') { + return '🎬'; + } else if (it.type == 'giphy') { + return '[GIF]'; + } + return it == lastMessage.attachments.last + ? (it.title ?? 'File') + : '${it.title ?? 'File'} , '; + }), + if (lastMessageText != null) lastMessageText, + ]; + + final fontStyle = (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal; + + final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle); + + final mentionsTextStyle = textStyle?.copyWith( + fontStyle: fontStyle, + fontWeight: FontWeight.bold, + ); + + final spans = [ + for (final part in messageTextParts) + if (lastMessageMentionedUsers.isNotEmpty && + lastMessageMentionedUsers.any((it) => '@${it.name}' == part)) + TextSpan( + text: '$part ', + style: mentionsTextStyle, + ) + else if (lastMessageAttachments.isNotEmpty && + lastMessageAttachments + .where((it) => it.title != null) + .any((it) => it.title == part)) + TextSpan( + text: '$part ', + style: regularTextStyle, + ) + else + TextSpan( + text: part == messageTextParts.last ? part : '$part ', + style: regularTextStyle, + ), + ]; + + return Text.rich( + TextSpan(children: spans), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart new file mode 100644 index 00000000..ea9f5c1e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -0,0 +1,369 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Signature for a function that creates a widget for a given index, e.g., in a +/// list. +/// +/// Used by [GridView.builder] and other APIs that use lazily-generated widgets. +/// +/// See also: +/// +/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. +/// * [TransitionBuilder], which is similar but also takes a child. +/// * [NullableIndexedWidgetBuilder], which is similar but may return null. +typedef StreamChannelListViewItemBuilder = Widget Function( + BuildContext context, + Channel channel, +); + +typedef StreamChannelTapCallback = void Function(Channel); + +Widget _defaultSeparatorBuilder(context, index) => + const _ChannelListSeparator(); + +class StreamChannelListView extends StatefulWidget { + const StreamChannelListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = _defaultSeparatorBuilder, + this.onChannelTap, + this.onChannelLongPress, + this.padding, + this.physics, + this.reverse = false, + this.scrollController, + this.primary, + this.scrollBehavior, + this.shrinkWrap = false, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + }) : super(key: key); + + final StreamChannelListController controller; + + final StreamChannelListViewItemBuilder? itemBuilder; + + final IndexedWidgetBuilder separatorBuilder; + + /// Called when the user taps this list tile. + /// + /// Inoperative if [enabled] is false. + final StreamChannelTapCallback? onChannelTap; + + /// Called when the user long-presses on this list tile. + /// + /// Inoperative if [enabled] is false. + final StreamChannelTapCallback? onChannelLongPress; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollController] is null. + final bool? primary; + + /// {@macro flutter.widgets.shadow.scrollBehavior} + /// + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + @override + _StreamChannelListViewState createState() => _StreamChannelListViewState(); +} + +class _StreamChannelListViewState extends State { + StreamChannelListController get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant StreamChannelListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => + PagedValueListenableBuilder( + valueListenable: widget.controller, + builder: (context, value, _) => value.when( + (channels, nextPageKey, error) { + if (channels.isEmpty) { + return const Center(child: Text('No channels')); + } + + return ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + itemCount: value.itemCount, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = value.itemCount - 3; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (value.hasNextPage && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (!value.hasError) { + await _controller.loadMore(nextPageKey!); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == channels.length) { + if (value.hasError) { + return _ChannelListLoadMoreError( + onTap: _controller.retry, + ); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: _ChannelListLoadMoreIndicator(), + ), + ); + } + + final channel = channels[index]; + final itemBuilder = widget.itemBuilder; + if (itemBuilder != null) return itemBuilder(context, channel); + + final onTap = widget.onChannelTap; + final onLongPress = widget.onChannelLongPress; + + return StreamChannelListTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: + onLongPress == null ? null : () => onLongPress(channel), + ); + }, + ); + }, + loading: () => ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => Center(child: Text('Error: $error')), + ), + ); +} + +class _ChannelListLoadMoreIndicator extends StatelessWidget { + const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), + ); +} + +class _ChannelListLoadMoreError extends StatelessWidget { + const _ChannelListLoadMoreError({ + Key? key, + required this.onTap, + }) : super(key: key); + + final GestureTapCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Container( + color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.translations.loadingChannelsError, + style: theme.textTheme.body.copyWith( + color: Colors.white, + ), + ), + StreamSvgIcon.retry(color: Colors.white), + ], + ), + ), + ), + ); + } +} + +class _ChannelListSeparator extends StatelessWidget { + const _ChannelListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart new file mode 100644 index 00000000..e1a8cdeb --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart @@ -0,0 +1,201 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) +/// +/// It shows the current [Channel] image. +/// +/// ```dart +/// class MyApp extends StatelessWidget { +/// final StreamChatClient client; +/// final Channel channel; +/// +/// MyApp(this.client, this.channel); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// debugShowCheckedModeBanner: false, +/// home: StreamChat( +/// client: client, +/// child: StreamChannel( +/// channel: channel, +/// child: Center( +/// child: ChannelImage( +/// channel: channel, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// +/// By default the widget radius size is 40x40 pixels. +/// Set the property [constraints] to set a custom dimension. +/// +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +class StreamChannelAvatar extends StatelessWidget { + /// Instantiate a new ChannelImage + StreamChannelAvatar({ + Key? key, + required this.channel, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// [BorderRadius] to display the widget + final BorderRadius? borderRadius; + + /// The channel to show the image of + final Channel channel; + + /// The diameter of the image + final BoxConstraints? constraints; + + /// The function called when the image is tapped + final VoidCallback? onTap; + + /// If image is selected + final bool selected; + + /// Selection color for image + final Color? selectionColor; + + /// Thickness of selection image + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final client = channel.client.state; + + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; + + return BetterStreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, channelImage) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + channel.name?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, + ), + ), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; + }, + noDataBuilder: (context) { + final currentUser = client.currentUser!; + final otherMembers = channel.state!.members + .where((it) => it.userId != currentUser.id) + .toList(growable: false); + + // our own space, no other members + if (otherMembers.isEmpty) { + return BetterStreamBuilder( + stream: client.currentUserStream.map((it) => it!), + initialData: currentUser, + builder: (context, user) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // 1-1 Conversation + if (otherMembers.length == 1) { + final member = otherMembers.first; + return BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // Group conversation + return GroupAvatar( + channel: channel, + members: otherMembers, + borderRadius: borderRadius ?? previewTheme?.borderRadius, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart new file mode 100644 index 00000000..2bcd56e8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// It shows the current [Channel] name using a [Text] widget. +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +class StreamChannelName extends StatelessWidget { + /// Instantiate a new ChannelName + StreamChannelName({ + Key? key, + required this.channel, + this.textStyle, + this.textOverflow = TextOverflow.ellipsis, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// How visual overflow should be handled. + final TextOverflow textOverflow; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + channel.client.state.currentUser!, + channel.state!.members, + ), + ); + + Widget _generateName( + User currentUser, + List members, + ) => + LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; + } + } else { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = + currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + } + + return Text( + channelName, + style: textStyle, + overflow: textOverflow, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index cd0d93a0..ce82789a 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -48,3 +48,5 @@ export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; export 'src/visible_footnote.dart'; +export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; From 2cab266c90ccd173edd3711994de2b957d25c99d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Nov 2021 17:33:19 +0530 Subject: [PATCH 02/19] feat(ui): add channel list event handler in channel list controller. Signed-off-by: xsahil03x --- .../lib/src/paged_value_notifier.dart | 70 +++- .../stream_channel_list_controller.dart | 213 +++++++++++- .../stream_channel_list_event_handler.dart | 311 ++++++++++++++++++ .../stream_channel_list_view.dart | 91 +++-- 4 files changed, 628 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index d71db24d..d2f5b373 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -10,6 +10,14 @@ const defaultInitialPagedLimitMultiplier = 3; typedef PagedValueListenableBuilder = ValueListenableBuilder>; +/// A [PagedValueNotifier] that uses a [PagedListenable] to load data. +/// +/// This class is useful when you need to load data from a server +/// using a [PagedListenable] and want to keep the UI-driven refresh +/// signals in the [PagedListenable]. +/// +/// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] +/// whenever the data is loaded or an error occurs. abstract class PagedValueNotifier extends ValueNotifier> { /// Creates a [PagedValueNotifier] @@ -18,14 +26,33 @@ abstract class PagedValueNotifier /// Stores initialValue in case we need to call [refresh]. final PagedValue _initialValue; + /// Returns the currently loaded items + List get currentItems => value.asSuccess.items; + + /// Appends [newItems] to the previously loaded ones and replaces + /// the next page's key. + void appendPage({ + required List newItems, + required Key nextPageKey, + }) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems, nextPageKey: nextPageKey); + } + + /// Appends [newItems] to the previously loaded ones and sets the next page + /// key to `null`. + void appendLastPage(List newItems) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems); + } + /// Retry any failed load requests. /// /// Unlike [refresh], this does not resets the whole [value], /// it only retries the last failed load request. Future retry() { - var lastValue = value; + final lastValue = value.asSuccess; assert(lastValue.hasError, ''); - lastValue = lastValue as Success; final nextPageKey = lastValue.nextPageKey; // resetting the error @@ -53,7 +80,7 @@ abstract class PagedValueNotifier abstract class PagedValue with _$PagedValue { const PagedValue._(); - /// Creates a new instance of [PagedValue] with the given [key] and [value]. + /// Represents the success state of the [PagedValue] // @Assert( // 'nextPageKey != null', // 'Cannot set an error if all the pages are already fetched', @@ -69,24 +96,35 @@ abstract class PagedValue with _$PagedValue { StreamChatError? error, }) = Success; - bool get hasNextPage { - assert(this is Success, ''); - return (this as Success).nextPageKey != null; + /// Represents the loading state of the [PagedValue]. + const factory PagedValue.loading() = Loading; + + /// Represents the error state of the [PagedValue]. + const factory PagedValue.error(StreamChatError error) = Error; + + /// Returns `true` if the [PagedValue] is [Success]. + bool get isSuccess => this is Success; + + /// Returns the [PagedValue] as [Success]. + Success get asSuccess { + assert( + isSuccess, + 'Cannot get asSuccess if the PagedValue is not in the Success state', + ); + return this as Success; } - bool get hasError { - assert(this is Success, ''); - return (this as Success).error != null; - } + /// Returns `true` if the [PagedValue] is [Success] + /// and has more items to load. + bool get hasNextPage => asSuccess.nextPageKey != null; + /// Returns `true` if the [PagedValue] is [Success] and has an error. + bool get hasError => asSuccess.error != null; + + /// int get itemCount { - assert(this is Success, ''); - final count = (this as Success).items.length; + final count = asSuccess.items.length; if (hasNextPage || hasError) return count + 1; return count; } - - const factory PagedValue.loading() = Loading; - - const factory PagedValue.error(StreamChatError error) = Error; } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 4bfc7130..d4715619 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -1,15 +1,40 @@ +import 'dart:async'; + import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' + as event_handler; +const defaultChannelPagedLimit = 10; + +typedef ChannelListEventHandler = void Function( + Event event, + StreamChannelListController controller, +); + +/// A controller for the channel list view. class StreamChannelListController extends PagedValueNotifier { /// Creates a [StreamChannelListController]. StreamChannelListController({ required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. @@ -18,9 +43,22 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(value); /// The client to use for the channel list. @@ -41,6 +79,82 @@ class StreamChannelListController extends PagedValueNotifier { /// The limit to apply to the member list. final int? memberLimit; + /// Callback function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// By default, calls [event_handler.onChannelDeleted] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelDeleted; + + /// Callback function which gets called for the event + /// [EventType.channelHidden]. + /// + /// By default, calls [event_handler.onChannelHidden] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelHidden; + + /// Callback function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// By default, calls [event_handler.onChannelTruncated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelTruncated; + + /// Callback function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// By default, calls [event_handler.onChannelUpdated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelUpdated; + + /// Callback function which gets called for the event + /// [EventType.channelVisible]. + /// + /// By default, calls [event_handler.onChannelVisible] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelVisible; + + /// Callback function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// By default, calls [event_handler.onConnectionRecovered] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onConnectionRecovered; + + /// Callback function which gets called for the event [EventType.messageNew]. + /// + /// By default, calls [event_handler.onMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// By default, calls [event_handler.onNotificationAddedToChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationAddedToChannel; + + /// Callback function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// By default, calls [event_handler.onNotificationMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// By default, calls [event_handler.onNotificationRemovedFromChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationRemovedFromChannel; + + /// Callback function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// By default, calls [event_handler.onUserPresenceChanged] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onUserPresenceChanged; + @override Future doInitialLoad() async { final limit = this.limit * defaultInitialPagedLimitMultiplier; @@ -58,15 +172,16 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = PagedValue.error(StreamChatError('error')); + // start listening events + _subscribeToChannelListEvents(); + } on StreamChatError catch (error) { + value = PagedValue.error(error); } } @override Future loadMore(int nextPageKey) async { - assert(value is Success, ''); - final previousValue = value as Success; + final previousValue = value.asSuccess; try { await for (final channels in client.queryChannels( @@ -84,8 +199,90 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = previousValue.copyWith(error: StreamChatError('error')); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); } } + + /// Replaces the previously loaded channels with [channels] and updates + /// the nextPageKey. + set channels(List channels) { + value = PagedValue( + items: channels, + nextPageKey: channels.length, + ); + } + + /// Returns/Creates a new Channel and starts watching it. + Future getChannel({ + required String id, + required String type, + }) async { + final channel = client.channel(type, id: id); + await channel.watch(); + return channel; + } + + StreamSubscription? _channelEventSubscription; + + // Subscribes to the channel list events. + void _subscribeToChannelListEvents() { + if (_channelEventSubscription != null) { + _unsubscribeFromChannelListEvents(); + } + + _channelEventSubscription = client.on().listen((event) { + final eventType = event.type; + if (eventType == EventType.channelDeleted) { + onChannelDeleted(event, this); + } else if (eventType == EventType.channelHidden) { + onChannelHidden(event, this); + } else if (eventType == EventType.channelTruncated) { + onChannelTruncated(event, this); + } else if (eventType == EventType.channelUpdated) { + onChannelUpdated(event, this); + } else if (eventType == EventType.channelVisible) { + onChannelVisible(event, this); + } else if (eventType == EventType.connectionRecovered) { + onConnectionRecovered(event, this); + } else if (eventType == EventType.connectionChanged) { + if (event.online != null) onConnectionRecovered(event, this); + } else if (eventType == EventType.messageNew) { + onMessageNew(event, this); + } else if (eventType == EventType.notificationAddedToChannel) { + onNotificationAddedToChannel(event, this); + } else if (eventType == EventType.notificationMessageNew) { + onNotificationMessageNew(event, this); + } else if (eventType == EventType.notificationRemovedFromChannel) { + onNotificationRemovedFromChannel(event, this); + } else if (eventType == 'user.presence.changed' || + eventType == EventType.userUpdated) { + onUserPresenceChanged(event, this); + } + }); + } + + // Unsubscribes from all channel list events. + void _unsubscribeFromChannelListEvents() { + if (_channelEventSubscription != null) { + _channelEventSubscription!.cancel(); + _channelEventSubscription = null; + } + } + + /// Pauses all subscriptions added to this composite. + void pauseEventsSubscription([Future? resumeSignal]) { + _channelEventSubscription?.pause(resumeSignal); + } + + /// Resumes all subscriptions added to this composite. + void resumeEventsSubscription() { + _channelEventSubscription?.resume(); + } + + @override + void dispose() { + _unsubscribeFromChannelListEvents(); + super.dispose(); + } } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart new file mode 100644 index 00000000..e35549a9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -0,0 +1,311 @@ +import 'package:stream_chat/stream_chat.dart' show Event; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Handles [EventType.channelDeleted] event. +/// +/// This event is fired when a channel is deleted. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelDeleted: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelDeleted( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); + + controller.channels = updatedChannels; +} + +/// Handles [EventType.channelHidden] event. +/// +/// This event is fired when a channel is hidden. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelHidden: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelHidden( + Event event, + StreamChannelListController controller, +) { + onChannelDeleted(event, controller); +} + +/// Handles [EventType.channelTruncated] event. +/// +/// This event is fired when a channel is truncated. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelTruncated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelTruncated( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.channelUpdated] event. +/// +/// This event is fired when a channel is updated. +/// +/// By default, this updates the channel received in the event. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelUpdated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelUpdated( + Event event, + StreamChannelListController controller, +) { + final eventChannel = event.channel; + if (eventChannel == null) return; + + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), + ); + + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } + + controller.channels = channels; +} + +/// Handles [EventType.channelVisible] event. +/// +/// This event is fired when a channel is made visible. +/// +/// By default, this adds the channel to the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelVisible: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelVisible( + Event event, + StreamChannelListController controller, +) async { + final channelId = event.channelId; + final channelType = event.channelType; + + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; +} + +/// Handles [EventType.connectionRecovered] event. +/// +/// This event is fired when the client web-socket connection recovers. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onConnectionRecovered: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onConnectionRecovered( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.messageNew] event. +/// +/// This event is fired when a new message is created in one of the channels +/// we are currently watching. +/// +/// By default, this moves the channel to the top of the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onMessageNew( + Event event, + StreamChannelListController controller, +) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; +} + +/// Handles [EventType.notificationAddedToChannel] event. +/// +/// This event is fired when a channel is added which we are not watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationAddedToChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationMessageNew] event. +/// +/// This event is fired when a new message is created in a channel which we are +/// not currently watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationMessageNew( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationRemovedFromChannel] event. +/// +/// This event is fired when a user is removed from a channel which we are +/// not currently watching. +/// +/// By default, this removes the event channel from the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationRemovedFromChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; +} + +/// Handles 'user.presence.changed' and [EventType.userUpdated] event. +/// +/// This event is fired when a user's presence changes or gets updated. +/// +/// By default, this updates the channel member with the event user. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onUserPresenceChanged: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onUserPresenceChanged( + Event event, + StreamChannelListController controller, +) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index ea9f5c1e..b774eaea 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -10,32 +10,44 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Signature for a function that creates a widget for a given index, e.g., in a -/// list. -/// -/// Used by [GridView.builder] and other APIs that use lazily-generated widgets. -/// -/// See also: -/// -/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. -/// * [TransitionBuilder], which is similar but also takes a child. -/// * [NullableIndexedWidgetBuilder], which is similar but may return null. +Widget defaultSeparatorBuilder(context, index) => + const StreamChannelListSeparator(); + typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, ); -typedef StreamChannelTapCallback = void Function(Channel); - -Widget _defaultSeparatorBuilder(context, index) => - const _ChannelListSeparator(); - +/// A [ListView] that shows a list of [Channel]s, +/// it uses [StreamChannelListTile] as a default item. +/// +/// This is the new version of [ChannelListView] that uses +/// [StreamChannelListController]. +/// +/// Example: +/// +/// ```dart +/// StreamChannelListView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelListTile] +/// * [StreamChannelListController] class StreamChannelListView extends StatefulWidget { + /// Creates a new instance of [StreamChannelListView]. const StreamChannelListView({ Key? key, required this.controller, this.itemBuilder, - this.separatorBuilder = _defaultSeparatorBuilder, + this.separatorBuilder = defaultSeparatorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -51,21 +63,23 @@ class StreamChannelListView extends StatefulWidget { this.restorationId, }) : super(key: key); + /// The [StreamChannelListController] used to control the list of channels. final StreamChannelListController controller; + /// A builder that is called to build items in the [ListView]. + /// + /// The `index` parameter is the index of the list tile in the list and the + /// `channel` parameter is the [Channel] at that position. final StreamChannelListViewItemBuilder? itemBuilder; + /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; /// Called when the user taps this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelTap; + final void Function(Channel)? onChannelTap; /// Called when the user long-presses on this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelLongPress; + final void Function(Channel)? onChannelLongPress; /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; @@ -254,11 +268,11 @@ class _StreamChannelListViewState extends State { final newPageRequestTriggerIndex = value.itemCount - 3; final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; - if (value.hasNextPage && isBuildingTriggerIndexItem) { + if (nextPageKey != null && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance?.addPostFrameCallback((_) async { if (!value.hasError) { - await _controller.loadMore(nextPageKey!); + await _controller.loadMore(nextPageKey); } _hasRequestedNextPage = false; }); @@ -267,15 +281,15 @@ class _StreamChannelListViewState extends State { } if (index == channels.length) { - if (value.hasError) { - return _ChannelListLoadMoreError( + if (error != null) { + return ChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: _ChannelListLoadMoreIndicator(), + child: ChannelListLoadMoreIndicator(), ), ); } @@ -309,8 +323,12 @@ class _StreamChannelListViewState extends State { ); } -class _ChannelListLoadMoreIndicator extends StatelessWidget { - const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +/// A [StreamChannelListTile] that can be used in a [ListView] to show a +/// loading tile while waiting for the [StreamChannelListController] to load +/// more channels. +class ChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreIndicator]. + const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -320,12 +338,16 @@ class _ChannelListLoadMoreIndicator extends StatelessWidget { ); } -class _ChannelListLoadMoreError extends StatelessWidget { - const _ChannelListLoadMoreError({ +/// A [StreamChannelListTile] that is used to display the error indicator when +/// loading more channels fails. +class ChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreError]. + const ChannelListLoadMoreError({ Key? key, required this.onTap, }) : super(key: key); + /// The callback to invoke when the user taps on the error indicator. final GestureTapCallback onTap; @override @@ -355,8 +377,11 @@ class _ChannelListLoadMoreError extends StatelessWidget { } } -class _ChannelListSeparator extends StatelessWidget { - const _ChannelListSeparator({Key? key}) : super(key: key); +/// A widget that is used to display a separator between +/// [StreamChannelListTile] items. +class StreamChannelListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSeparator]. + const StreamChannelListSeparator({Key? key}) : super(key: key); @override Widget build(BuildContext context) { From 6466d58156269ca5936a37396f746d5a271e1c0e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Nov 2021 18:01:16 +0530 Subject: [PATCH 03/19] chore(ui): add docs Signed-off-by: xsahil03x --- .../stream_channel_list_loading_tile.dart | 5 ++ .../stream_channel_list_tile.dart | 53 ++++++++----------- .../stream_channel_list_view.dart | 9 ++-- .../lib/src/v4/stream_channel_name.dart | 1 + 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart index 9e0231d5..69a6cf6e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart @@ -2,7 +2,12 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// A shimmering list item which shows a loading effect. +/// +/// This is used by [StreamChannelListView] to show a loading effect while +/// the list is being loaded. class StreamChannelListLoadingTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadingTile] widget. const StreamChannelListLoadingTile({ Key? key, this.visualDensity = VisualDensity.standard, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index f4fb4fde..a3d0f67a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat/stream_chat.dart' show Channel; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/sending_indicator.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/channel_preview_theme.dart'; @@ -10,9 +11,20 @@ import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +/// A widget that displays a channel preview. +/// +/// This widget is intended to be used as a Tile in [StreamChannelListView] +/// +/// It shows the last message of the channel, the last message time, the unread +/// message count, the typing indicator, the sending indicator and the channel +/// avatar. +/// +/// See also: +/// * [StreamChannelAvatar] +/// * [StreamChannelName] class StreamChannelListTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTile] widget. StreamChannelListTile({ Key? key, required this.channel, @@ -29,50 +41,22 @@ class StreamChannelListTile extends StatelessWidget { ), super(key: key); + /// The channel to display. final Channel channel; /// A widget to display before the title. - /// - /// Typically an [Icon] or a [CircleAvatar] widget. final Widget? leading; /// The primary content of the list tile. - /// - /// Typically a [Text] widget. - /// - /// This should not wrap. To enforce the single line limit, use - /// [Text.maxLines]. final Widget? title; /// Additional content displayed below the title. - /// - /// Typically a [Text] widget. - /// - /// If [isThreeLine] is false, this should not wrap. - /// - /// If [isThreeLine] is true, this should be configured to take a maximum of - /// two lines. For example, you can use [Text.maxLines] to enforce the number - /// of lines. - /// - /// The subtitle's default [TextStyle] depends on [TextTheme.bodyText2] except - /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled] - /// and [selected]. - /// - /// When [enabled] is false, the text color is set to [ThemeData.disabledColor]. - /// - /// When [selected] is false, the text color is set to [ListTileTheme.textColor] - /// if it's not null and to [TextTheme.caption]'s color if [ListTileTheme.textColor] - /// is null. final Widget? subtitle; /// Called when the user taps this list tile. - /// - /// Inoperative if [enabled] is false. final GestureTapCallback? onTap; /// Called when the user long-presses on this list tile. - /// - /// Inoperative if [enabled] is false. final GestureLongPressCallback? onLongPress; /// Defines how compact the list tile's layout will be. @@ -196,7 +180,9 @@ class StreamChannelListTile extends StatelessWidget { } } +/// A widget that displays the channel last message date. class ChannelLastMessageDate extends StatelessWidget { + /// Creates a new instance of the [ChannelLastMessageDate] widget. ChannelLastMessageDate({ Key? key, required this.channel, @@ -207,6 +193,7 @@ class ChannelLastMessageDate extends StatelessWidget { ), super(key: key); + /// The channel to display the last message date for. final Channel channel; /// The style of the text displayed @@ -246,7 +233,9 @@ class ChannelLastMessageDate extends StatelessWidget { ); } +/// A widget that displays the subtitle for [StreamChannelListTile]. class ChannelListTileSubtitle extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTileSubtitle] widget. ChannelListTileSubtitle({ Key? key, required this.channel, @@ -257,6 +246,7 @@ class ChannelListTileSubtitle extends StatelessWidget { ), super(key: key); + /// The channel to create the subtitle from. final Channel channel; /// The style of the text displayed @@ -287,7 +277,9 @@ class ChannelListTileSubtitle extends StatelessWidget { } } +/// A widget that displays the last message of a channel. class ChannelLastMessageText extends StatelessWidget { + /// Creates a new instance of [ChannelLastMessageText] widget. ChannelLastMessageText({ Key? key, required this.channel, @@ -298,6 +290,7 @@ class ChannelLastMessageText extends StatelessWidget { ), super(key: key); + /// The channel to display the last message of. final Channel channel; /// The style of the text displayed diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index b774eaea..af493815 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -10,9 +10,12 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -Widget defaultSeparatorBuilder(context, index) => +/// Default separator builder for [StreamChannelListView]. +Widget defaultSeparatorBuilder(BuildContext context, int index) => const StreamChannelListSeparator(); +/// Signature for the item builder that creates the children of the +/// [StreamChannelListView]. typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, @@ -265,13 +268,13 @@ class _StreamChannelListViewState extends State { separatorBuilder: widget.separatorBuilder, itemBuilder: (context, index) { if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = value.itemCount - 3; + final newPageRequestTriggerIndex = channels.length - 3; final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; if (nextPageKey != null && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance?.addPostFrameCallback((_) async { - if (!value.hasError) { + if (error == null) { await _controller.loadMore(nextPageKey); } _hasRequestedNextPage = false; diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart index 2bcd56e8..7d3be9a7 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -21,6 +21,7 @@ class StreamChannelName extends StatelessWidget { ), super(key: key); + /// The [Channel] to show the name for. final Channel channel; /// The style of the text displayed From 430a92375a58f3f5b2a8079d16e14d3f2b77fa21 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 29 Nov 2021 16:40:44 +0530 Subject: [PATCH 04/19] chore(ui): add support for presence. Signed-off-by: xsahil03x --- .../stream_channel_list_controller.dart | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index d4715619..de6196cd 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -12,13 +12,14 @@ typedef ChannelListEventHandler = void Function( StreamChannelListController controller, ); -/// A controller for the channel list view. +/// class StreamChannelListController extends PagedValueNotifier { - /// Creates a [StreamChannelListController]. + /// Creates a new instance of [StreamChannelListController]. StreamChannelListController({ required this.client, this.filter, this.sort, + this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, @@ -43,6 +44,7 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, this.filter, this.sort, + this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, @@ -64,21 +66,31 @@ class StreamChannelListController extends PagedValueNotifier { /// The client to use for the channel list. final StreamChatClient client; - /// The filter to apply to the channel list. + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. final Filter? filter; - /// The sort to apply to the channel list. + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, + /// created_at or member_count. + /// Direction can be ascending or descending. final List>? sort; + /// If true you’ll receive user presence updates via the websocket events + final bool presence; + /// The limit to apply to the channel list. final int limit; - /// The limit to apply to the message list. - final int? messageLimit; - - /// The limit to apply to the member list. + /// Number of members to fetch in each channel final int? memberLimit; + /// Number of messages to fetch in each channel + final int? messageLimit; + /// Callback function which gets called for the event /// [EventType.channelDeleted]. /// @@ -164,6 +176,7 @@ class StreamChannelListController extends PagedValueNotifier { sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, + presence: presence, paginationParams: PaginationParams(limit: limit), )) { final nextKey = channels.length < limit ? null : channels.length; @@ -176,6 +189,9 @@ class StreamChannelListController extends PagedValueNotifier { _subscribeToChannelListEvents(); } on StreamChatError catch (error) { value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); } } @@ -189,6 +205,7 @@ class StreamChannelListController extends PagedValueNotifier { sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, + presence: presence, paginationParams: PaginationParams(limit: limit, offset: nextPageKey), )) { final previousItems = previousValue.items; @@ -201,6 +218,9 @@ class StreamChannelListController extends PagedValueNotifier { } } on StreamChatError catch (error) { value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); } } From 042315353ced6e541a202ff1e145af3a4a851fc7 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 2 Dec 2021 12:34:58 +0100 Subject: [PATCH 05/19] refactor: add ChannelEvents class and remove event callbacks --- .../example/lib/tutorial_part_2.dart | 2 +- .../v4/channel_list_view/channel_events.dart | 123 +++++++++++ .../stream_channel_list_controller.dart | 191 ++++++------------ .../lib/stream_chat_flutter.dart | 5 +- 4 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6e1edaba..f704abab 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -70,7 +70,7 @@ class MyApp extends StatelessWidget { } class ChannelListPage extends StatefulWidget { - ChannelListPage({ + const ChannelListPage({ Key? key, required this.client, }) : super(key: key); diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart new file mode 100644 index 00000000..05c2bdbd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart @@ -0,0 +1,123 @@ +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' + as event_handler; + +/// Contains methods that are called for certain [Event]s. These methods are +/// called from the [StreamChannelListController]. +/// +/// This class can be mixed in or extended to create custom overrides. +class ChannelEvents { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// By default, calls [event_handler.onChannelDeleted] + /// with the [Event] and the [StreamChannelListController]. + void onChannelDeleted(Event event, StreamChannelListController controller) { + event_handler.onChannelDeleted(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// By default, calls [event_handler.onChannelHidden] + /// with the [Event] and the [StreamChannelListController]. + void onChannelHidden(Event event, StreamChannelListController controller) { + event_handler.onChannelHidden(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// By default, calls [event_handler.onChannelTruncated] + /// with the [Event] and the [StreamChannelListController]. + void onChannelTruncated(Event event, StreamChannelListController controller) { + event_handler.onChannelTruncated(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// By default, calls [event_handler.onChannelUpdated] + /// with the [Event] and the [StreamChannelListController]. + void onChannelUpdated(Event event, StreamChannelListController controller) { + event_handler.onChannelUpdated(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// By default, calls [event_handler.onChannelVisible] + /// with the [Event] and the [StreamChannelListController]. + void onChannelVisible(Event event, StreamChannelListController controller) { + event_handler.onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// By default, calls [event_handler.onConnectionRecovered] + /// with the [Event] and the [StreamChannelListController]. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + event_handler.onConnectionRecovered(event, controller); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// By default, calls [event_handler.onMessageNew] + /// with the [Event] and the [StreamChannelListController]. + void onMessageNew(Event event, StreamChannelListController controller) { + event_handler.onMessageNew(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// By default, calls [event_handler.onNotificationAddedToChannel] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationAddedToChannel(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// By default, calls [event_handler.onNotificationMessageNew] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationMessageNew(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// By default, calls [event_handler.onNotificationRemovedFromChannel] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationRemovedFromChannel(event, controller); + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// By default, calls [event_handler.onUserPresenceChanged] + /// with the [Event] and the [StreamChannelListController]. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + event_handler.onUserPresenceChanged(event, controller); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index de6196cd..3ea6a614 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,68 +2,72 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' - as event_handler; +import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_events.dart'; +/// The default channel page limit to load. const defaultChannelPagedLimit = 10; -typedef ChannelListEventHandler = void Function( - Event event, - StreamChannelListController controller, -); - -/// +/// A controller for a Channel list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded channels. +/// * Return/Create a new channel and start watching it. +/// * Unsubscribe from all channel list events. +/// * Pause and Resume all subscriptions added to this composite. class StreamChannelListController extends PagedValueNotifier { - /// Creates a new instance of [StreamChannelListController]. + /// Creates a Stream channel list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `channelEvents` is the channel events to use for the channels list. + /// This class can be mixed in or extended to create custom overrides. See + /// [ChannelEvents] for advice. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the channels matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the channel list. + /// + /// * `messageLimit` is the number of messages to fetch in each channel. + /// + /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, + ChannelEvents? channelEvents, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - this.onChannelDeleted = event_handler.onChannelDeleted, - this.onChannelHidden = event_handler.onChannelHidden, - this.onChannelTruncated = event_handler.onChannelTruncated, - this.onChannelUpdated = event_handler.onChannelUpdated, - this.onChannelVisible = event_handler.onChannelVisible, - this.onConnectionRecovered = event_handler.onConnectionRecovered, - this.onMessageNew = event_handler.onMessageNew, - this.onNotificationAddedToChannel = - event_handler.onNotificationAddedToChannel, - this.onNotificationMessageNew = event_handler.onNotificationMessageNew, - this.onNotificationRemovedFromChannel = - event_handler.onNotificationRemovedFromChannel, - this.onUserPresenceChanged = event_handler.onUserPresenceChanged, - }) : super(const PagedValue.loading()); + }) : channelEvents = channelEvents ?? ChannelEvents(), + super(const PagedValue.loading()) { + this.channelEvents.test(); + } /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, + required this.channelEvents, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - this.onChannelDeleted = event_handler.onChannelDeleted, - this.onChannelHidden = event_handler.onChannelHidden, - this.onChannelTruncated = event_handler.onChannelTruncated, - this.onChannelUpdated = event_handler.onChannelUpdated, - this.onChannelVisible = event_handler.onChannelVisible, - this.onConnectionRecovered = event_handler.onConnectionRecovered, - this.onMessageNew = event_handler.onMessageNew, - this.onNotificationAddedToChannel = - event_handler.onNotificationAddedToChannel, - this.onNotificationMessageNew = event_handler.onNotificationMessageNew, - this.onNotificationRemovedFromChannel = - event_handler.onNotificationRemovedFromChannel, - this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(value); - /// The client to use for the channel list. + /// The channel events to use for the channels list. + final ChannelEvents channelEvents; + + /// The client to use for the channels list. final StreamChatClient client; /// The query filters to use. @@ -82,90 +86,15 @@ class StreamChannelListController extends PagedValueNotifier { /// If true you’ll receive user presence updates via the websocket events final bool presence; - /// The limit to apply to the channel list. + /// The limit to apply to the channel list. The default is set to + /// [defaultChannelPagedLimit]. final int limit; - /// Number of members to fetch in each channel - final int? memberLimit; - - /// Number of messages to fetch in each channel + /// Number of messages to fetch in each channel. final int? messageLimit; - /// Callback function which gets called for the event - /// [EventType.channelDeleted]. - /// - /// By default, calls [event_handler.onChannelDeleted] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelDeleted; - - /// Callback function which gets called for the event - /// [EventType.channelHidden]. - /// - /// By default, calls [event_handler.onChannelHidden] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelHidden; - - /// Callback function which gets called for the event - /// [EventType.channelTruncated]. - /// - /// By default, calls [event_handler.onChannelTruncated] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelTruncated; - - /// Callback function which gets called for the event - /// [EventType.channelUpdated]. - /// - /// By default, calls [event_handler.onChannelUpdated] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelUpdated; - - /// Callback function which gets called for the event - /// [EventType.channelVisible]. - /// - /// By default, calls [event_handler.onChannelVisible] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelVisible; - - /// Callback function which gets called for the event - /// [EventType.connectionRecovered]. - /// - /// By default, calls [event_handler.onConnectionRecovered] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onConnectionRecovered; - - /// Callback function which gets called for the event [EventType.messageNew]. - /// - /// By default, calls [event_handler.onMessageNew] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onMessageNew; - - /// Callback function which gets called for the event - /// [EventType.notificationAddedToChannel]. - /// - /// By default, calls [event_handler.onNotificationAddedToChannel] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationAddedToChannel; - - /// Callback function which gets called for the event - /// [EventType.notificationMessageNew]. - /// - /// By default, calls [event_handler.onNotificationMessageNew] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationMessageNew; - - /// Callback function which gets called for the event - /// [EventType.notificationRemovedFromChannel]. - /// - /// By default, calls [event_handler.onNotificationRemovedFromChannel] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationRemovedFromChannel; - - /// Callback function which gets called for the event - /// 'user.presence.changed' and [EventType.userUpdated]. - /// - /// By default, calls [event_handler.onUserPresenceChanged] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onUserPresenceChanged; + /// Number of members to fetch in each channel. + final int? memberLimit; @override Future doInitialLoad() async { @@ -185,7 +114,7 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - // start listening events + // start listening to events _subscribeToChannelListEvents(); } on StreamChatError catch (error) { value = PagedValue.error(error); @@ -254,30 +183,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - onChannelDeleted(event, this); + channelEvents.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - onChannelHidden(event, this); + channelEvents.onChannelDeleted(event, this); } else if (eventType == EventType.channelTruncated) { - onChannelTruncated(event, this); + channelEvents.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - onChannelUpdated(event, this); + channelEvents.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - onChannelVisible(event, this); + channelEvents.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - onConnectionRecovered(event, this); + channelEvents.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { - if (event.online != null) onConnectionRecovered(event, this); + if (event.online != null) { + channelEvents.onConnectionRecovered(event, this); + } } else if (eventType == EventType.messageNew) { - onMessageNew(event, this); + channelEvents.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - onNotificationAddedToChannel(event, this); + channelEvents.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - onNotificationMessageNew(event, this); + channelEvents.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - onNotificationRemovedFromChannel(event, this); + channelEvents.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - onUserPresenceChanged(event, this); + channelEvents.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index ce82789a..b1e25b9e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,6 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/visible_footnote.dart'; -export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/channel_list_view/channel_events.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; +export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/visible_footnote.dart'; From 5b0536349735c224dd78911af6fe819c952f999b Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 2 Dec 2021 16:50:20 +0100 Subject: [PATCH 06/19] refactor: rename ChannelEvents to ChannelEventHandlers --- ...vents.dart => channel_event_handlers.dart} | 6 +-- .../stream_channel_list_controller.dart | 48 +++++++++---------- .../lib/stream_chat_flutter.dart | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) rename packages/stream_chat_flutter/lib/src/v4/channel_list_view/{channel_events.dart => channel_event_handlers.dart} (96%) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart rename to packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart index 05c2bdbd..57b7d8c8 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart @@ -3,11 +3,11 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' as event_handler; -/// Contains methods that are called for certain [Event]s. These methods are -/// called from the [StreamChannelListController]. +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. /// /// This class can be mixed in or extended to create custom overrides. -class ChannelEvents { +class ChannelEventHandlers { /// Function which gets called for the event /// [EventType.channelDeleted]. /// diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 3ea6a614..73cfa2ee 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_events.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_event_handlers.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; @@ -11,6 +11,7 @@ const defaultChannelPagedLimit = 10; /// /// This class lets you perform tasks such as: /// * Load initial data. +/// * Use channel events handlers. /// * Load more data using [loadMore]. /// * Replace the previously loaded channels. /// * Return/Create a new channel and start watching it. @@ -21,9 +22,9 @@ class StreamChannelListController extends PagedValueNotifier { /// /// * `client` is the Stream chat client to use for the channels list. /// - /// * `channelEvents` is the channel events to use for the channels list. - /// This class can be mixed in or extended to create custom overrides. See - /// [ChannelEvents] for advice. + /// * `channelEventHandlers` is the channel events to use for the channels + /// list. This class can be mixed in or extended to create custom overrides. + /// See [ChannelEventHandlers] for advice. /// /// * `filter` is the query filters to use. /// @@ -39,33 +40,32 @@ class StreamChannelListController extends PagedValueNotifier { /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, - ChannelEvents? channelEvents, + ChannelEventHandlers? channelEventHandlers, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : channelEvents = channelEvents ?? ChannelEvents(), - super(const PagedValue.loading()) { - this.channelEvents.test(); - } + }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, - required this.channelEvents, + ChannelEventHandlers? channelEventHandlers, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : super(value); + }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + super(value); /// The channel events to use for the channels list. - final ChannelEvents channelEvents; + final ChannelEventHandlers _channelEventHandlers; /// The client to use for the channels list. final StreamChatClient client; @@ -183,32 +183,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - channelEvents.onChannelDeleted(event, this); + _channelEventHandlers.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - channelEvents.onChannelDeleted(event, this); + _channelEventHandlers.onChannelHidden(event, this); } else if (eventType == EventType.channelTruncated) { - channelEvents.onChannelTruncated(event, this); + _channelEventHandlers.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - channelEvents.onChannelUpdated(event, this); + _channelEventHandlers.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - channelEvents.onChannelVisible(event, this); + _channelEventHandlers.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - channelEvents.onConnectionRecovered(event, this); + _channelEventHandlers.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { if (event.online != null) { - channelEvents.onConnectionRecovered(event, this); + _channelEventHandlers.onConnectionRecovered(event, this); } } else if (eventType == EventType.messageNew) { - channelEvents.onMessageNew(event, this); + _channelEventHandlers.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - channelEvents.onNotificationAddedToChannel(event, this); + _channelEventHandlers.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - channelEvents.onNotificationMessageNew(event, this); + _channelEventHandlers.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - channelEvents.onNotificationRemovedFromChannel(event, this); + _channelEventHandlers.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - channelEvents.onUserPresenceChanged(event, this); + _channelEventHandlers.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b1e25b9e..76e3bc0e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/v4/channel_list_view/channel_events.dart'; +export 'src/v4/channel_list_view/channel_event_handlers.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; export 'src/visible_footnote.dart'; From d4bde539e6079134fb84d64f18beb5ff8ca1b2bd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 3 Dec 2021 18:52:11 +0530 Subject: [PATCH 07/19] refactor(llc): minor event handler changes. Signed-off-by: xsahil03x --- .../channel_event_handlers.dart | 123 ----- .../stream_channel_list_controller.dart | 48 +- .../stream_channel_list_event_handler.dart | 492 +++++++----------- .../lib/stream_chat_flutter.dart | 2 +- 4 files changed, 224 insertions(+), 441 deletions(-) delete mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart deleted file mode 100644 index 57b7d8c8..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:stream_chat/stream_chat.dart' hide Success; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' - as event_handler; - -/// Contains handlers that are called from [StreamChannelListController] for -/// certain [Event]s. -/// -/// This class can be mixed in or extended to create custom overrides. -class ChannelEventHandlers { - /// Function which gets called for the event - /// [EventType.channelDeleted]. - /// - /// By default, calls [event_handler.onChannelDeleted] - /// with the [Event] and the [StreamChannelListController]. - void onChannelDeleted(Event event, StreamChannelListController controller) { - event_handler.onChannelDeleted(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelHidden]. - /// - /// By default, calls [event_handler.onChannelHidden] - /// with the [Event] and the [StreamChannelListController]. - void onChannelHidden(Event event, StreamChannelListController controller) { - event_handler.onChannelHidden(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelTruncated]. - /// - /// By default, calls [event_handler.onChannelTruncated] - /// with the [Event] and the [StreamChannelListController]. - void onChannelTruncated(Event event, StreamChannelListController controller) { - event_handler.onChannelTruncated(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelUpdated]. - /// - /// By default, calls [event_handler.onChannelUpdated] - /// with the [Event] and the [StreamChannelListController]. - void onChannelUpdated(Event event, StreamChannelListController controller) { - event_handler.onChannelUpdated(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelVisible]. - /// - /// By default, calls [event_handler.onChannelVisible] - /// with the [Event] and the [StreamChannelListController]. - void onChannelVisible(Event event, StreamChannelListController controller) { - event_handler.onChannelVisible(event, controller); - } - - /// Function which gets called for the event - /// [EventType.connectionRecovered]. - /// - /// By default, calls [event_handler.onConnectionRecovered] - /// with the [Event] and the [StreamChannelListController]. - void onConnectionRecovered( - Event event, - StreamChannelListController controller, - ) { - event_handler.onConnectionRecovered(event, controller); - } - - /// Function which gets called for the event [EventType.messageNew]. - /// - /// By default, calls [event_handler.onMessageNew] - /// with the [Event] and the [StreamChannelListController]. - void onMessageNew(Event event, StreamChannelListController controller) { - event_handler.onMessageNew(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationAddedToChannel]. - /// - /// By default, calls [event_handler.onNotificationAddedToChannel] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationAddedToChannel( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationAddedToChannel(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationMessageNew]. - /// - /// By default, calls [event_handler.onNotificationMessageNew] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationMessageNew( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationMessageNew(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationRemovedFromChannel]. - /// - /// By default, calls [event_handler.onNotificationRemovedFromChannel] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationRemovedFromChannel( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationRemovedFromChannel(event, controller); - } - - /// Function which gets called for the event - /// 'user.presence.changed' and [EventType.userUpdated]. - /// - /// By default, calls [event_handler.onUserPresenceChanged] - /// with the [Event] and the [StreamChannelListController]. - void onUserPresenceChanged( - Event event, - StreamChannelListController controller, - ) { - event_handler.onUserPresenceChanged(event, controller); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 73cfa2ee..5dcbeaa2 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_event_handlers.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; @@ -15,7 +15,6 @@ const defaultChannelPagedLimit = 10; /// * Load more data using [loadMore]. /// * Replace the previously loaded channels. /// * Return/Create a new channel and start watching it. -/// * Unsubscribe from all channel list events. /// * Pause and Resume all subscriptions added to this composite. class StreamChannelListController extends PagedValueNotifier { /// Creates a Stream channel list controller. @@ -24,7 +23,7 @@ class StreamChannelListController extends PagedValueNotifier { /// /// * `channelEventHandlers` is the channel events to use for the channels /// list. This class can be mixed in or extended to create custom overrides. - /// See [ChannelEventHandlers] for advice. + /// See [StreamChannelListEventHandler] for advice. /// /// * `filter` is the query filters to use. /// @@ -40,46 +39,51 @@ class StreamChannelListController extends PagedValueNotifier { /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, - ChannelEventHandlers? channelEventHandlers, + StreamChannelListEventHandler? eventHandler, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, - ChannelEventHandlers? channelEventHandlers, + StreamChannelListEventHandler? eventHandler, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(value); - /// The channel events to use for the channels list. - final ChannelEventHandlers _channelEventHandlers; - /// The client to use for the channels list. final StreamChatClient client; + /// The channel event handlers to use for the channels list. + final StreamChannelListEventHandler _eventHandler; + /// The query filters to use. + /// /// You can query on any of the custom fields you've defined on the [Channel]. + /// /// You can also filter other built-in channel fields. final Filter? filter; /// The sorting used for the channels matching the filters. + /// /// Sorting is based on field and direction, multiple sorting options /// can be provided. + /// /// You can sort based on last_updated, last_message_at, updated_at, /// created_at or member_count. + /// /// Direction can be ascending or descending. final List>? sort; @@ -183,32 +187,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - _channelEventHandlers.onChannelDeleted(event, this); + _eventHandler.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - _channelEventHandlers.onChannelHidden(event, this); + _eventHandler.onChannelHidden(event, this); } else if (eventType == EventType.channelTruncated) { - _channelEventHandlers.onChannelTruncated(event, this); + _eventHandler.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - _channelEventHandlers.onChannelUpdated(event, this); + _eventHandler.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - _channelEventHandlers.onChannelVisible(event, this); + _eventHandler.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - _channelEventHandlers.onConnectionRecovered(event, this); + _eventHandler.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { if (event.online != null) { - _channelEventHandlers.onConnectionRecovered(event, this); + _eventHandler.onConnectionRecovered(event, this); } } else if (eventType == EventType.messageNew) { - _channelEventHandlers.onMessageNew(event, this); + _eventHandler.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - _channelEventHandlers.onNotificationAddedToChannel(event, this); + _eventHandler.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - _channelEventHandlers.onNotificationMessageNew(event, this); + _eventHandler.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - _channelEventHandlers.onNotificationRemovedFromChannel(event, this); + _eventHandler.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - _channelEventHandlers.onUserPresenceChanged(event, this); + _eventHandler.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index e35549a9..bd6cf50d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -1,311 +1,213 @@ -import 'package:stream_chat/stream_chat.dart' show Event; +import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Handles [EventType.channelDeleted] event. +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. /// -/// This event is fired when a channel is deleted. -/// -/// By default, this removes the channel from the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelDeleted: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelDeleted( - Event event, - StreamChannelListController controller, -) { - final channels = [...controller.currentItems]; +/// This class can be mixed in or extended to create custom overrides. +class StreamChannelListEventHandler { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// This event is fired when a channel is deleted. + /// + /// By default, this removes the channel from the list of channels. + void onChannelDeleted(Event event, StreamChannelListController controller) { + final channels = [...controller.currentItems]; - final updatedChannels = channels - ..removeWhere( - (it) => it.cid == (event.cid ?? event.channel?.cid), - ); + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); - controller.channels = updatedChannels; -} - -/// Handles [EventType.channelHidden] event. -/// -/// This event is fired when a channel is hidden. -/// -/// By default, this removes the channel from the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelHidden: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelHidden( - Event event, - StreamChannelListController controller, -) { - onChannelDeleted(event, controller); -} - -/// Handles [EventType.channelTruncated] event. -/// -/// This event is fired when a channel is truncated. -/// -/// By default, this refreshes the whole channel list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelTruncated: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelTruncated( - Event event, - StreamChannelListController controller, -) { - controller.refresh(); -} - -/// Handles [EventType.channelUpdated] event. -/// -/// This event is fired when a channel is updated. -/// -/// By default, this updates the channel received in the event. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelUpdated: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelUpdated( - Event event, - StreamChannelListController controller, -) { - final eventChannel = event.channel; - if (eventChannel == null) return; - - final channels = [...controller.currentItems]; - final channelIndex = channels.indexWhere( - (it) => it.cid == (event.cid ?? eventChannel.cid), - ); - - if (channelIndex >= 0) { - final channelState = ChannelState(channel: eventChannel); - channels[channelIndex].state?.updateChannelState(channelState); + controller.channels = updatedChannels; } - controller.channels = channels; -} + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// This event is fired when a channel is hidden. + /// + /// By default, this removes the channel from the list of channels. + void onChannelHidden(Event event, StreamChannelListController controller) { + onChannelDeleted(event, controller); + } -/// Handles [EventType.channelVisible] event. -/// -/// This event is fired when a channel is made visible. -/// -/// By default, this adds the channel to the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelVisible: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelVisible( - Event event, - StreamChannelListController controller, -) async { - final channelId = event.channelId; - final channelType = event.channelType; + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// This event is fired when a channel is truncated. + /// + /// By default, this refreshes the whole channel list. + void onChannelTruncated(Event event, StreamChannelListController controller) { + controller.refresh(); + } - if (channelId == null || channelType == null) return; + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// This event is fired when a channel is updated. + /// + /// By default, this updates the channel received in the event. + void onChannelUpdated(Event event, StreamChannelListController controller) { + final eventChannel = event.channel; + if (eventChannel == null) return; - final channel = await controller.getChannel( - id: channelId, - type: channelType, - ); - - final currentChannels = [...controller.currentItems]; - - final updatedChannels = [ - channel, - ...currentChannels..removeWhere((it) => it.cid == channel.cid), - ]; - - controller.channels = updatedChannels; -} - -/// Handles [EventType.connectionRecovered] event. -/// -/// This event is fired when the client web-socket connection recovers. -/// -/// By default, this refreshes the whole channel list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onConnectionRecovered: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onConnectionRecovered( - Event event, - StreamChannelListController controller, -) { - controller.refresh(); -} - -/// Handles [EventType.messageNew] event. -/// -/// This event is fired when a new message is created in one of the channels -/// we are currently watching. -/// -/// By default, this moves the channel to the top of the list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onMessageNew: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onMessageNew( - Event event, - StreamChannelListController controller, -) { - final channelCid = event.cid; - if (channelCid == null) return; - - final channels = [...controller.currentItems]; - - final channelIndex = channels.indexWhere((it) => it.cid == channelCid); - if (channelIndex <= 0) return; - - final channel = channels.removeAt(channelIndex); - channels.insert(0, channel); - - controller.channels = [...channels]; -} - -/// Handles [EventType.notificationAddedToChannel] event. -/// -/// This event is fired when a channel is added which we are not watching. -/// -/// By default, this adds the channel and moves it to the top of list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationAddedToChannel: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationAddedToChannel( - Event event, - StreamChannelListController controller, -) { - onChannelVisible(event, controller); -} - -/// Handles [EventType.notificationMessageNew] event. -/// -/// This event is fired when a new message is created in a channel which we are -/// not currently watching. -/// -/// By default, this adds the channel and moves it to the top of list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationMessageNew: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationMessageNew( - Event event, - StreamChannelListController controller, -) { - onChannelVisible(event, controller); -} - -/// Handles [EventType.notificationRemovedFromChannel] event. -/// -/// This event is fired when a user is removed from a channel which we are -/// not currently watching. -/// -/// By default, this removes the event channel from the list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationRemovedFromChannel: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationRemovedFromChannel( - Event event, - StreamChannelListController controller, -) { - final channels = [...controller.currentItems]; - final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); - final listChanged = channels.length != updatedChannels.length; - - if (!listChanged) return; - - controller.channels = [...updatedChannels]; -} - -/// Handles 'user.presence.changed' and [EventType.userUpdated] event. -/// -/// This event is fired when a user's presence changes or gets updated. -/// -/// By default, this updates the channel member with the event user. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onUserPresenceChanged: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onUserPresenceChanged( - Event event, - StreamChannelListController controller, -) { - final user = event.user; - if (user == null) return; - - final channels = [...controller.currentItems]; - - final updatedChannels = channels.map((channel) { - final members = [...channel.state!.members]; - final memberIndex = members.indexWhere( - (it) => user.id == (it.userId ?? it.user?.id), + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), ); - if (memberIndex < 0) return channel; + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } - members[memberIndex] = members[memberIndex].copyWith(user: user); - final updatedState = ChannelState(members: [...members]); - channel.state!.updateChannelState(updatedState); + controller.channels = channels; + } - return channel; - }); + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// This event is fired when a channel is made visible. + /// + /// By default, this adds the channel to the list of channels. + void onChannelVisible( + Event event, + StreamChannelListController controller, + ) async { + final channelId = event.channelId; + final channelType = event.channelType; - controller.channels = [...updatedChannels]; + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// This event is fired when the client web-socket connection recovers. + /// + /// By default, this refreshes the whole channel list. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + controller.refresh(); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// This event is fired when a new message is created in one of the channels + /// we are currently watching. + /// + /// By default, this moves the channel to the top of the list. + void onMessageNew(Event event, StreamChannelListController controller) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// This event is fired when a channel is added which we are not watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// This event is fired when a new message is created in a channel which we are + /// not currently watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// This event is fired when a user is removed from a channel which we are + /// not currently watching. + /// + /// By default, this removes the event channel from the list. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + final channels = [...controller.currentItems]; + final updatedChannels = + channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// This event is fired when a user's presence changes or gets updated. + /// + /// By default, this updates the channel member with the event user. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; + } } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 76e3bc0e..a9d1416b 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/v4/channel_list_view/channel_event_handlers.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; +export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; export 'src/visible_footnote.dart'; From f3110e99ebfbf4ebc96cb6c0a994e126736f5269 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 6 Dec 2021 17:28:40 +0530 Subject: [PATCH 08/19] refactor(ui): add channel list empty and error state widgets. Signed-off-by: xsahil03x --- .../stream_channel_list_event_handler.dart | 2 +- .../stream_channel_list_view.dart | 106 +++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index bd6cf50d..449dd982 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; /// Contains handlers that are called from [StreamChannelListController] for diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index af493815..67d4ad56 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -250,7 +250,12 @@ class _StreamChannelListViewState extends State { builder: (context, value, _) => value.when( (channels, nextPageKey, error) { if (channels.isEmpty) { - return const Center(child: Text('No channels')); + return const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmpty(), + ), + ); } return ListView.separated( @@ -321,7 +326,11 @@ class _StreamChannelListViewState extends State { separatorBuilder: widget.separatorBuilder, itemBuilder: (_, __) => const StreamChannelListLoadingTile(), ), - error: (error) => Center(child: Text('Error: $error')), + error: (error) => Center( + child: StreamChannelListError( + onPressed: _controller.refresh, + ), + ), ), ); } @@ -347,11 +356,11 @@ class ChannelListLoadMoreError extends StatelessWidget { /// Creates a new instance of [ChannelListLoadMoreError]. const ChannelListLoadMoreError({ Key? key, - required this.onTap, + this.onTap, }) : super(key: key); /// The callback to invoke when the user taps on the error indicator. - final GestureTapCallback onTap; + final GestureTapCallback? onTap; @override Widget build(BuildContext context) { @@ -395,3 +404,92 @@ class StreamChannelListSeparator extends StatelessWidget { ); } } + +/// A widget that is used to display an error screen +/// when [StreamChannelListController] fails to load initial channels. +class StreamChannelListError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListError] widget. + const StreamChannelListError({ + Key? key, + this.onPressed, + }) : super(key: key); + + /// The callback to invoke when the user taps on the retry button. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(right: 2), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: context.translations.loadingChannelsError), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + TextButton( + onPressed: onPressed, + child: Text(context.translations.retryLabel), + ), + ], + ); +} + +/// A widget that is used to display an empty state when +/// [StreamChannelListController] loads zero channels. +class StreamChannelListEmpty extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmpty] widget. + const StreamChannelListEmpty({ + Key? key, + this.onPressed, + }) : super(key: key); + + /// The callback to invoke when the user taps on the start a chat button. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + const SizedBox(height: 28), + Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, + ), + const SizedBox(height: 8), + Text( + context.translations.sendingFirstMessageLabel, + textAlign: TextAlign.center, + style: chatThemeData.textTheme.body.copyWith( + color: chatThemeData.colorTheme.textLowEmphasis, + ), + ), + const Spacer(), + TextButton( + onPressed: onPressed, + child: Text( + context.translations.startAChatLabel, + style: chatThemeData.textTheme.bodyBold.copyWith( + color: chatThemeData.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + } +} From 093a66854641f331ad00eee68fb48cb10092a214 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:48:57 +0530 Subject: [PATCH 09/19] refactor(ui): add stream_channel_info_bottom_sheet.dart Signed-off-by: xsahil03x --- .../lib/src/paged_value_notifier.dart | 6 +- .../v4/stream_channel_info_bottom_sheet.dart | 365 ++++++++++++++++++ .../lib/stream_chat_flutter.dart | 3 + 3 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index d2f5b373..5ef37374 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -62,10 +62,12 @@ abstract class PagedValueNotifier /// Refresh the data presented by this [PagedValueNotifier]. /// + /// Resets the [value] to the initial value in case [resetValue] is true. + /// /// Note: This API is intended for UI-driven refresh signals, /// such as swipe-to-refresh. - Future refresh() { - value = _initialValue; + Future refresh({bool resetValue = true}) { + if (resetValue) value = _initialValue; return doInitialLoad(); } diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart new file mode 100644 index 00000000..51bda67d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -0,0 +1,365 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; +import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; + +/// A [BottomSheet] that shows information about a [Channel]. +class StreamChannelInfoBottomSheet extends StatelessWidget { + /// Creates a new instance [StreamChannelInfoBottomSheet] widget. + StreamChannelInfoBottomSheet({ + Key? key, + required this.channel, + this.onMemberTap, + this.onViewInfoTap, + this.onLeaveChannelTap, + this.onDeleteConversationTap, + this.onCancelTap, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show information about. + final Channel channel; + + /// A callback that is called when a member is tapped. + final void Function(Member)? onMemberTap; + + /// A callback that is called when the "View Info" button is tapped. + final VoidCallback? onViewInfoTap; + + /// A callback that is called when the "Leave Channel" button is tapped. + /// + /// Only shown when the channel is a group channel. + final VoidCallback? onLeaveChannelTap; + + /// A callback that is called when the "Delete Conversation" button is tapped. + /// + /// Only shown when you are the `owner` of the channel. + final VoidCallback? onDeleteConversationTap; + + /// A callback that is called when the "Cancel" button is tapped. + final VoidCallback? onCancelTap; + + @override + Widget build(BuildContext context) { + final themeData = StreamChatTheme.of(context); + final colorTheme = themeData.colorTheme; + final channelPreviewTheme = ChannelPreviewTheme.of(context); + + final currentUser = channel.client.state.currentUser; + final isOneToOneChannel = channel.isDistinct && channel.memberCount == 2; + + final members = channel.state?.members ?? []; + + final isOwner = members.any( + (it) => it.user?.id == currentUser?.id && it.role == 'owner', + ); + + // remove current user in case it's 1-1 conversation + if (isOneToOneChannel) { + members.removeWhere((it) => it.user?.id == currentUser?.id); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamChannelName( + channel: channel, + textStyle: themeData.textTheme.headlineBold, + ), + ), + ), + const SizedBox(height: 5), + Center( + // TODO: Refactor ChannelInfo + child: ChannelInfo( + showTypingIndicator: false, + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ), + ), + const SizedBox(height: 17), + Container( + height: 94, + alignment: Alignment.center, + child: ListView.separated( + shrinkWrap: true, + itemCount: members.length, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemBuilder: (context, index) { + final member = members[index]; + final user = member.user!; + return Column( + children: [ + UserAvatar( + user: user, + constraints: const BoxConstraints( + maxHeight: 64, + maxWidth: 64, + ), + borderRadius: BorderRadius.circular(32), + onlineIndicatorConstraints: BoxConstraints.tight( + const Size(12, 12), + ), + onTap: onMemberTap != null + ? (_) => onMemberTap!(member) + : null, + ), + const SizedBox(height: 6), + Text( + user.name, + style: themeData.textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24), + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.user( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.viewInfoLabel, + onTap: onViewInfoTap, + ), + if (!isOneToOneChannel) + OptionListTile( + title: context.translations.leaveGroupLabel, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.userRemove( + color: colorTheme.textLowEmphasis, + ), + ), + onTap: onLeaveChannelTap, + ), + if (isOwner) + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.delete( + color: colorTheme.accentError, + ), + ), + title: context.translations.deleteConversationLabel, + titleColor: colorTheme.accentError, + onTap: onDeleteConversationTap, + ), + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.closeSmall( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.cancelLabel, + onTap: onCancelTap ?? Navigator.of(context).pop, + ), + ], + ); + } +} + +const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), +); + +/// Shows a modal material design bottom sheet. +/// +/// A modal bottom sheet is an alternative to a menu or a dialog and prevents +/// the user from interacting with the rest of the app. +/// +/// A closely related widget is a persistent bottom sheet, which shows +/// information that supplements the primary content of the app without +/// preventing the use from interacting with the app. Persistent bottom sheets +/// can be created and displayed with the [showBottomSheet] function or the +/// [ScaffoldState.showBottomSheet] method. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the bottom sheet. It is only used when the method is called. Its +/// corresponding widget can be safely removed from the tree before the bottom +/// sheet is closed. +/// +/// The `isScrollControlled` parameter specifies whether this is a route for +/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish +/// to have a bottom sheet that has a scrollable child such as a [ListView] or +/// a [GridView] and have the bottom sheet be draggable, you should set this +/// parameter to true. +/// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// modal bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// The [transitionAnimationController] controls the bottom sheet's entrance and +/// exit animations if provided. +/// +/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet +/// sheet. This is particularly useful in the case that a user wants to observe +/// [PopupRoute]s within a [NavigatorObserver]. +/// +/// Returns a `Future` that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal bottom sheet was closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// function passed as the `builder` argument to [showModalBottomSheet]. +/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing +/// non-modal bottom sheets. +/// * [DraggableScrollableSheet], which allows you to create a bottom sheet +/// that grows and then becomes scrollable once it reaches its maximum size. +/// * +Future showChannelInfoModalBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = true, + bool useRootNavigator = false, + bool isDismissible = true, + bool enableDrag = true, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showModalBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); + +/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If +/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet]. +/// +/// Returns a controller that can be used to close and otherwise manipulate the +/// bottom sheet. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// persistent bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// To rebuild the bottom sheet (e.g. if it is stateful), call +/// [PersistentBottomSheetController.setState] on the controller returned by +/// this method. +/// +/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing +/// [ModalRoute] and a back button is added to the app bar of the [Scaffold] +/// that closes the bottom sheet. +/// +/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and +/// does not add a back button to the enclosing Scaffold's app bar, use the +/// [Scaffold.bottomSheet] constructor parameter. +/// +/// A closely related widget is a modal bottom sheet, which is an alternative +/// to a menu or a dialog and prevents the user from interacting with the rest +/// of the app. Modal bottom sheets can be created and displayed with the +/// [showModalBottomSheet] function. +/// +/// The `context` argument is used to look up the [Scaffold] for the bottom +/// sheet. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the bottom sheet is closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// `builder`. +/// * [showModalBottomSheet], which can be used to display a modal bottom +/// sheet. +/// * [Scaffold.of], for information about how to obtain the [BuildContext]. +/// * +PersistentBottomSheetController showChannelInfoBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index a9d1416b..749a1d63 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,10 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; + +// v4 export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/stream_channel_info_bottom_sheet.dart'; export 'src/visible_footnote.dart'; From c301b51c8381e59e34f78656d1d6fb1df0c86d4e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:49:50 +0530 Subject: [PATCH 10/19] refactor(ui): improve channel list and controller Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 8 ++ .../stream_channel_list_controller.dart | 29 ++++ .../stream_channel_list_tile.dart | 84 ++++++++--- .../stream_channel_list_view.dart | 136 ++++++++++-------- 4 files changed, 178 insertions(+), 79 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 51034d3a..e2660099 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1833,6 +1833,14 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); + /// Channel member for the current user. + Member? get currentUserMember => members.firstWhereOrNull( + (m) => m.user?.id == _channel.client.state.currentUser?.id, + ); + + /// User role for the current user. + String? get currentUserRole => currentUserMember?.role; + /// Channel read list. List get read => _channelState.read; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 5dcbeaa2..c94ac616 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -176,6 +176,32 @@ class StreamChannelListController extends PagedValueNotifier { return channel; } + /// Leaves the [channel] and updates the list. + Future leaveChannel(Channel channel) async { + final user = client.state.currentUser; + assert(user != null, 'You must be logged in to leave a channel.'); + await channel.removeMembers([user!.id]); + } + + /// Deletes the [channel] and updates the list. + Future deleteChannel(Channel channel) async { + await channel.delete(); + } + + /// Mutes the [channel] and updates the list. + Future muteChannel(Channel channel) async { + await channel.mute(); + } + + /// Un-mutes the [channel] and updates the list. + Future unmuteChannel(Channel channel) async { + await channel.unmute(); + } + + /// Event listener, which can be set in order to listen + /// [client] web-socket events. + bool Function(Event event)? eventListener; + StreamSubscription? _channelEventSubscription; // Subscribes to the channel list events. @@ -185,6 +211,9 @@ class StreamChannelListController extends PagedValueNotifier { } _channelEventSubscription = client.on().listen((event) { + // Returns early if the event is already handled by the listener. + if (eventListener?.call(event) ?? false) return; + final eventType = event.type; if (eventType == EventType.channelDeleted) { _eventHandler.onChannelDeleted(event, this); diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index a3d0f67a..d58e1441 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -31,10 +31,14 @@ class StreamChannelListTile extends StatelessWidget { this.leading, this.title, this.subtitle, + this.trailing, this.onTap, this.onLongPress, + this.tileColor, this.visualDensity = VisualDensity.compact, this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.unreadIndicatorBuilder, + this.sendingIndicatorBuilder, }) : assert( channel.state != null, 'Channel ${channel.id} is not initialized', @@ -53,12 +57,23 @@ class StreamChannelListTile extends StatelessWidget { /// Additional content displayed below the title. final Widget? subtitle; + /// A widget to display at the end of tile. + final Widget? trailing; + /// Called when the user taps this list tile. final GestureTapCallback? onTap; /// Called when the user long-presses on this list tile. final GestureLongPressCallback? onLongPress; + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile` when [selected] is false. + /// + /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + /// Defines how compact the list tile's layout will be. /// /// {@macro flutter.material.themedata.visualDensity} @@ -77,6 +92,40 @@ class StreamChannelListTile extends StatelessWidget { /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. final EdgeInsetsGeometry contentPadding; + /// The widget builder for the unread indicator. + final WidgetBuilder? unreadIndicatorBuilder; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel, Use it to determine the + /// status using [Message.status]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListTile copyWith({ + Key? key, + Channel? channel, + Widget? leading, + Widget? title, + Widget? subtitle, + VoidCallback? onTap, + VoidCallback? onLongPress, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + @override Widget build(BuildContext context) { final channelState = channel.state!; @@ -101,6 +150,12 @@ class StreamChannelListTile extends StatelessWidget { textStyle: channelPreviewTheme.subtitleStyle, ); + final trailing = this.trailing ?? + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ); + return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, @@ -113,6 +168,7 @@ class StreamChannelListTile extends StatelessWidget { visualDensity: visualDensity, contentPadding: contentPadding, leading: leading, + tileColor: tileColor, title: Row( children: [ Expanded(child: title), @@ -125,7 +181,8 @@ class StreamChannelListTile extends StatelessWidget { !members.any((it) => it.user!.id == currentUser.id)) { return const Offstage(); } - return UnreadIndicator(cid: channel.cid); + return unreadIndicatorBuilder?.call(context) ?? + UnreadIndicator(cid: channel.cid); }, ), ], @@ -154,24 +211,19 @@ class StreamChannelListTile extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 4), - child: SendingIndicator( - message: lastMessage, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channelState.read - .where((it) => it.user.id != currentUser.id) - .where( - (it) => it.lastRead.isAfter(lastMessage.createdAt), - ) - .isNotEmpty, - ), + child: + sendingIndicatorBuilder?.call(context, lastMessage) ?? + SendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), + ), ); }, ), - ChannelLastMessageDate( - channel: channel, - textStyle: channelPreviewTheme.lastMessageAtStyle, - ), - // trailing ?? _buildDate(context), + trailing, ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index 67d4ad56..1527493e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -19,6 +19,7 @@ Widget defaultSeparatorBuilder(BuildContext context, int index) => typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, + StreamChannelListTile defaultWidget, ); /// A [ListView] that shows a list of [Channel]s, @@ -51,6 +52,9 @@ class StreamChannelListView extends StatefulWidget { required this.controller, this.itemBuilder, this.separatorBuilder = defaultSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -71,13 +75,29 @@ class StreamChannelListView extends StatefulWidget { /// A builder that is called to build items in the [ListView]. /// - /// The `index` parameter is the index of the list tile in the list and the - /// `channel` parameter is the [Channel] at that position. + /// The `channel` parameter is the [Channel] at this position in the list + /// and the `defaultWidget` is the default widget used + /// i.e: [StreamChannelListTile]. final StreamChannelListViewItemBuilder? itemBuilder; /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; + /// A builder that is called to build the empty state of the list. + /// + /// If not provider, [StreamChannelListEmptyWidget] will be used. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + /// + /// If not provided, [StreamChannelListLoadingTile] will be used. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + /// + /// If not provided, [StreamChannelListErrorWidget] will be used. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + /// Called when the user taps this list tile. final void Function(Channel)? onChannelTap; @@ -250,12 +270,13 @@ class _StreamChannelListViewState extends State { builder: (context, value, _) => value.when( (channels, nextPageKey, error) { if (channels.isEmpty) { - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamChannelListEmpty(), - ), - ); + return widget.emptyBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmptyWidget(), + ), + ); } return ListView.separated( @@ -290,47 +311,61 @@ class _StreamChannelListViewState extends State { if (index == channels.length) { if (error != null) { - return ChannelListLoadMoreError( + return StreamChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: ChannelListLoadMoreIndicator(), + child: StreamChannelListLoadMoreIndicator(), ), ); } final channel = channels[index]; - final itemBuilder = widget.itemBuilder; - if (itemBuilder != null) return itemBuilder(context, channel); final onTap = widget.onChannelTap; final onLongPress = widget.onChannelLongPress; - return StreamChannelListTile( + final streamChannelListTile = StreamChannelListTile( channel: channel, onTap: onTap == null ? null : () => onTap(channel), onLongPress: onLongPress == null ? null : () => onLongPress(channel), ); + + final itemBuilder = widget.itemBuilder; + + if (itemBuilder != null) { + return itemBuilder( + context, + channel, + streamChannelListTile, + ); + } + + return streamChannelListTile; }, ); }, - loading: () => ListView.separated( - padding: widget.padding, - physics: widget.physics, - reverse: widget.reverse, - itemCount: 25, - separatorBuilder: widget.separatorBuilder, - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), - ), - error: (error) => Center( - child: StreamChannelListError( - onPressed: _controller.refresh, - ), - ), + loading: () => + widget.loadingBuilder?.call(context) ?? + ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => + widget.errorBuilder?.call(context, error) ?? + Center( + child: StreamChannelListErrorWidget( + onPressed: _controller.refresh, + ), + ), ), ); } @@ -338,9 +373,9 @@ class _StreamChannelListViewState extends State { /// A [StreamChannelListTile] that can be used in a [ListView] to show a /// loading tile while waiting for the [StreamChannelListController] to load /// more channels. -class ChannelListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreIndicator]. - const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +class StreamChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreIndicator]. + const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -352,9 +387,9 @@ class ChannelListLoadMoreIndicator extends StatelessWidget { /// A [StreamChannelListTile] that is used to display the error indicator when /// loading more channels fails. -class ChannelListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreError]. - const ChannelListLoadMoreError({ +class StreamChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreError]. + const StreamChannelListLoadMoreError({ Key? key, this.onTap, }) : super(key: key); @@ -407,9 +442,9 @@ class StreamChannelListSeparator extends StatelessWidget { /// A widget that is used to display an error screen /// when [StreamChannelListController] fails to load initial channels. -class StreamChannelListError extends StatelessWidget { - /// Creates a new instance of [StreamChannelListError] widget. - const StreamChannelListError({ +class StreamChannelListErrorWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListErrorWidget] widget. + const StreamChannelListErrorWidget({ Key? key, this.onPressed, }) : super(key: key); @@ -445,15 +480,9 @@ class StreamChannelListError extends StatelessWidget { /// A widget that is used to display an empty state when /// [StreamChannelListController] loads zero channels. -class StreamChannelListEmpty extends StatelessWidget { - /// Creates a new instance of [StreamChannelListEmpty] widget. - const StreamChannelListEmpty({ - Key? key, - this.onPressed, - }) : super(key: key); - - /// The callback to invoke when the user taps on the start a chat button. - final VoidCallback? onPressed; +class StreamChannelListEmptyWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmptyWidget] widget. + const StreamChannelListEmptyWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -461,7 +490,6 @@ class StreamChannelListEmpty extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Spacer(), StreamSvgIcon.message( size: 148, color: chatThemeData.colorTheme.disabled, @@ -471,24 +499,6 @@ class StreamChannelListEmpty extends StatelessWidget { context.translations.letsStartChattingLabel, style: chatThemeData.textTheme.headline, ), - const SizedBox(height: 8), - Text( - context.translations.sendingFirstMessageLabel, - textAlign: TextAlign.center, - style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - const Spacer(), - TextButton( - onPressed: onPressed, - child: Text( - context.translations.startAChatLabel, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ), ], ); } From 51e7c873cb1cd89fc7193ac9bda000da311ac22a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:55:55 +0530 Subject: [PATCH 11/19] refactor(ui): minor doc changes Signed-off-by: xsahil03x --- .../v4/channel_list_view/stream_channel_list_controller.dart | 3 +++ .../lib/src/v4/channel_list_view/stream_channel_list_tile.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index c94ac616..488452bd 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -200,6 +200,9 @@ class StreamChannelListController extends PagedValueNotifier { /// Event listener, which can be set in order to listen /// [client] web-socket events. + /// + /// Return `true` if the event is handled. Return `false` to + /// allow the event to be handled internally. bool Function(Event event)? eventListener; StreamSubscription? _channelEventSubscription; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index d58e1441..685c556c 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -67,7 +67,7 @@ class StreamChannelListTile extends StatelessWidget { final GestureLongPressCallback? onLongPress; /// {@template flutter.material.ListTile.tileColor} - /// Defines the background color of `ListTile` when [selected] is false. + /// Defines the background color of `ListTile`. /// /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] /// if it's not null and to [Colors.transparent] if it's null. From 96fae22655d8f15ba0815eddddd69d209d80ebe0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 18 Jan 2022 16:39:07 +0100 Subject: [PATCH 12/19] update example --- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/lib/tutorial_part_4.dart | 16 +++++++++++++--- .../lib/src/paged_value_notifier.dart | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 1721e6f1..4b1a8d5d 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index 5ef37374..525e663d 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -123,7 +123,7 @@ abstract class PagedValue with _$PagedValue { /// Returns `true` if the [PagedValue] is [Success] and has an error. bool get hasError => asSuccess.error != null; - /// + /// int get itemCount { final count = asSuccess.items.length; if (hasNextPage || hasError) return count + 1; From f53ee0b4f10a71390a16f0a12e53aff1f5143461 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 10:52:59 +0100 Subject: [PATCH 13/19] fix analysis and update examples --- .../example/lib/split_view.dart | 34 +++++---- .../example/lib/tutorial_part_1.dart | 25 +++---- .../example/lib/tutorial_part_2.dart | 54 ++++++-------- .../example/lib/tutorial_part_3.dart | 59 ++++++++++----- .../example/lib/tutorial_part_4.dart | 56 +++++++------- .../example/lib/tutorial_part_5.dart | 46 ++++++++---- .../example/lib/tutorial_part_6.dart | 74 +++++++++++-------- .../lib/src/group_avatar.dart | 1 + .../lib/src/paged_value_notifier.dart | 8 +- .../stream_channel_list_controller.dart | 8 +- .../stream_channel_list_event_handler.dart | 4 +- .../stream_channel_list_tile.dart | 3 +- .../stream_channel_list_view.dart | 6 +- .../v4/stream_channel_info_bottom_sheet.dart | 5 +- .../lib/stream_chat_flutter.dart | 4 + 15 files changed, 223 insertions(+), 164 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 1fd3ccb6..75b8f64b 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -84,7 +84,7 @@ class _SplitViewState extends State { ); } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, this.onTap, @@ -92,22 +92,26 @@ class ChannelListPage extends StatelessWidget { final void Function(Channel)? onTap; + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + @override Widget build(BuildContext context) => Scaffold( - body: ChannelsBloc( - child: ChannelListView( - onChannelTap: onTap != null - ? (channel, _) { - onTap!(channel); - } - : null, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - ), + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, ), ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 459d8690..82acabf9 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index f704abab..9f718b7e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -92,26 +92,23 @@ class _ChannelListPageState extends State { ); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: RefreshIndicator( - onRefresh: _controller.refresh, - child: StreamChannelListView( - controller: _controller, - onChannelTap: (channel) => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => StreamChannel( - channel: channel, - child: const ChannelPage(), + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), ), ), ), ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -120,18 +117,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 538dbd68..e8834eb1 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -68,31 +68,49 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - channelPreviewBuilder: _channelPreviewBuilder, - // sort: [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); - } + State createState() => _ChannelListPageState(); +} - Widget _channelPreviewBuilder(BuildContext context, Channel channel) { +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + itemBuilder: _channelPreviewBuilder, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); + + Widget _channelPreviewBuilder( + BuildContext context, + Channel channel, + StreamChannelListTile defaultTile, + ) { final lastMessage = channel.state?.messages.reversed.firstWhereOrNull( (message) => !message.isDeleted, ); @@ -115,13 +133,14 @@ class ChannelListPage extends StatelessWidget { leading: ChannelAvatar( channel: channel, ), - title: ChannelName( + title: StreamChannelName( textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis .withOpacity(opacity), ), + channel: channel, ), subtitle: Text(subtitle), trailing: channel.state!.unreadCount > 0 diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 5b80b721..ce461ddc 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -53,38 +53,42 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: StreamChannelListView( - controller: StreamChannelListController( - client: StreamChat.of(context).client, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - ), - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), ), - ), - ); - }, - ), - ); - } + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 86ac2e1f..347ceaf4 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -59,28 +59,42 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 2555e007..4bb8e928 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -93,28 +93,41 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -123,24 +136,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const MessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index d7996c4d..e0e87e23 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -16,6 +16,7 @@ class GroupAvatar extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + /// The channel of the avatar final Channel? channel; /// List of images to display diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index 525e663d..9525fea4 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -5,8 +5,10 @@ import 'package:stream_chat/stream_chat.dart' show StreamChatError; part 'paged_value_notifier.freezed.dart'; +/// Default initial page size multiplier. const defaultInitialPagedLimitMultiplier = 3; +/// Value listenable for paged data. typedef PagedValueListenableBuilder = ValueListenableBuilder>; @@ -57,6 +59,7 @@ abstract class PagedValueNotifier final nextPageKey = lastValue.nextPageKey; // resetting the error value = lastValue.copyWith(error: null); + // ignore: null_check_on_nullable_type_parameter return loadMore(nextPageKey!); } @@ -78,10 +81,9 @@ abstract class PagedValueNotifier Future loadMore(Key nextPageKey); } +/// Paged value that can be used with [PagedValueNotifier]. @freezed abstract class PagedValue with _$PagedValue { - const PagedValue._(); - /// Represents the success state of the [PagedValue] // @Assert( // 'nextPageKey != null', @@ -98,6 +100,8 @@ abstract class PagedValue with _$PagedValue { StreamChatError? error, }) = Success; + const PagedValue._(); + /// Represents the loading state of the [PagedValue]. const factory PagedValue.loading() = Loading; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 488452bd..e7eb903e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; @@ -7,6 +8,8 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list /// The default channel page limit to load. const defaultChannelPagedLimit = 10; +const _kDefaultBackendPaginationLimit = 30; + /// A controller for a Channel list. /// /// This class lets you perform tasks such as: @@ -102,7 +105,10 @@ class StreamChannelListController extends PagedValueNotifier { @override Future doInitialLoad() async { - final limit = this.limit * defaultInitialPagedLimitMultiplier; + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); try { await for (final channels in client.queryChannels( filter: filter, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index 449dd982..44ace69e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -146,8 +146,8 @@ class StreamChannelListEventHandler { /// Function which gets called for the event /// [EventType.notificationMessageNew]. /// - /// This event is fired when a new message is created in a channel which we are - /// not currently watching. + /// This event is fired when a new message is created in a channel + /// which we are not currently watching. /// /// By default, this adds the channel and moves it to the top of list. void onNotificationMessageNew( diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index 3c392a8f..f820e040 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -68,7 +68,8 @@ class StreamChannelListTile extends StatelessWidget { /// {@template flutter.material.ListTile.tileColor} /// Defines the background color of `ListTile`. /// - /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] /// if it's not null and to [Colors.transparent] if it's null. /// {@endtemplate} final Color? tileColor; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index 1527493e..adf69be0 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -143,9 +143,9 @@ class StreamChannelListView extends StatefulWidget { /// only scroll the view if it has sufficient content. See [physics]. /// /// Also when true, the scroll view is used for default [ScrollAction]s. If a - /// ScrollAction is not handled by an otherwise focused part of the application, - /// the ScrollAction will be evaluated using this scroll view, for example, - /// when executing [Shortcuts] key events like page up and down. + /// ScrollAction is not handled by an otherwise focused part of the + /// application, the ScrollAction will be evaluated using this scroll view, + /// for example, when executing [Shortcuts] key events like page up and down. /// /// On iOS, this also identifies the scroll view that will scroll to top in /// response to a tap in the status bar. diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart index 51bda67d..5107131d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -229,8 +229,9 @@ const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( /// The [transitionAnimationController] controls the bottom sheet's entrance and /// exit animations if provided. /// -/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet -/// sheet. This is particularly useful in the case that a user wants to observe +/// The optional `routeSettings` parameter sets the [RouteSettings] +/// of the modal bottom sheet sheet. +/// This is particularly useful in the case that a user wants to observe /// [PopupRoute]s within a [NavigatorObserver]. /// /// Returns a `Future` that resolves to the value (if any) that was passed to diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index fab90d17..7e6dc769 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -57,6 +57,10 @@ export 'src/utils.dart'; // v4 export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; +export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/stream_channel_avatar.dart'; export 'src/v4/stream_channel_info_bottom_sheet.dart'; +export 'src/v4/stream_channel_name.dart'; export 'src/visible_footnote.dart'; From e80cebe9aa631605a6f27c70d6756b1c57f97573 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 11:57:26 +0100 Subject: [PATCH 14/19] deprecate old widgets --- .../example/lib/tutorial_part_3.dart | 2 +- .../example/lib/tutorial_part_4.dart | 29 +++++++++---------- .../lib/src/channel_avatar.dart | 5 ++++ .../lib/src/channel_list_view.dart | 4 +++ .../lib/src/channel_name.dart | 4 +++ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index e8834eb1..76447b11 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -130,7 +130,7 @@ class _ChannelListPageState extends State { ), ); }, - leading: ChannelAvatar( + leading: StreamChannelAvatar( channel: channel, ), title: StreamChannelName( diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index ce461ddc..fdea19ba 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -97,24 +97,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const MessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index ba24a640..50466040 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -44,6 +44,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. + +@Deprecated( + "'ChannelName' is deprecated and shouldn't be used. " + "Please use 'StreamChannelName' instead.", +) class ChannelAvatar extends StatelessWidget { /// Instantiate a new ChannelImage const ChannelAvatar({ diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index e4097690..270c29af 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -52,6 +52,10 @@ typedef ViewInfoCallback = void Function(Channel); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. +@Deprecated( + "'ChannelListView' is deprecated and shouldn't be used. " + "Please use 'StreamChannelListView' instead.", +) class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView ChannelListView({ diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index a1e81005..8dd8c054 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -6,6 +6,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// The widget uses a [StreamBuilder] to render the channel information /// image as soon as it updates. +@Deprecated( + "'ChannelName' is deprecated and shouldn't be used. " + "Please use 'StreamChannelName' instead.", +) class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ From 6351fa0a5f8aef763399eca49a20f1b9f6223ec0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 12:03:29 +0100 Subject: [PATCH 15/19] deprecate old widgets --- packages/stream_chat_flutter/lib/src/channel_avatar.dart | 4 ++-- .../stream_chat_flutter/lib/src/channel_bottom_sheet.dart | 5 +++++ packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 50466040..f7c949ee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -46,8 +46,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Modify it to change the widget appearance. @Deprecated( - "'ChannelName' is deprecated and shouldn't be used. " - "Please use 'StreamChannelName' instead.", + "'ChannelAvatar' is deprecated and shouldn't be used. " + "Please use 'StreamChannelAvatar' instead.", ) class ChannelAvatar extends StatelessWidget { /// Instantiate a new ChannelImage diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 92726939..05d5996e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -4,6 +4,10 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Bottom Sheet with options +@Deprecated( + "'ChannelBottomSheet' is deprecated and shouldn't be used. " + "Please use 'StreamChannelBottomSheet' instead.", +) class ChannelBottomSheet extends StatefulWidget { /// Constructor for creating bottom sheet const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); @@ -15,6 +19,7 @@ class ChannelBottomSheet extends StatefulWidget { _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); } +// ignore: deprecated_member_use_from_same_package class _ChannelBottomSheetState extends State { bool _showActions = true; diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index fa19a90f..255eeae4 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -16,6 +16,10 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ +@Deprecated( + "'ChannelsBloc' is deprecated and shouldn't be used. " + "Please use 'StreamChannelListView' instead.", +) class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and /// not null. From ce22e854a811179cf63239cf87ebabbcf5346b2d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 12:29:52 +0100 Subject: [PATCH 16/19] Update packages/stream_chat_flutter_core/lib/src/channels_bloc.dart Co-authored-by: Sahil Kumar --- packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 255eeae4..9e7b05d1 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -18,7 +18,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ @Deprecated( "'ChannelsBloc' is deprecated and shouldn't be used. " - "Please use 'StreamChannelListView' instead.", + "Please use 'StreamChannelListController' instead.", ) class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and From 202af02f8e7d6d456a66e3ddd313cb434efa53a8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 7 Feb 2022 15:25:07 +0530 Subject: [PATCH 17/19] chore: move controllers from ui to core Signed-off-by: xsahil03x --- .../src/message_input/simple_safe_area.dart | 17 +++----- .../stream_channel_list_view.dart | 3 -- .../lib/stream_chat_flutter.dart | 4 -- .../message_input_controller_test.dart | 14 ------ .../lib/src}/message_input_controller.dart | 4 +- .../src}/message_text_field_controller.dart | 15 +------ .../lib/src/paged_value_notifier.dart | 0 .../lib/src/paged_value_notifier.freezed.dart | 43 +++++++++---------- .../src}/stream_channel_list_controller.dart | 5 ++- .../stream_channel_list_event_handler.dart | 2 +- .../lib/stream_chat_flutter_core.dart | 5 +++ .../stream_chat_flutter_core/pubspec.yaml | 3 ++ 12 files changed, 44 insertions(+), 71 deletions(-) delete mode 100644 packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart rename packages/{stream_chat_flutter/lib/src/message_input => stream_chat_flutter_core/lib/src}/message_input_controller.dart (98%) rename packages/{stream_chat_flutter/lib/src/message_input => stream_chat_flutter_core/lib/src}/message_text_field_controller.dart (82%) rename packages/{stream_chat_flutter => stream_chat_flutter_core}/lib/src/paged_value_notifier.dart (100%) rename packages/{stream_chat_flutter => stream_chat_flutter_core}/lib/src/paged_value_notifier.freezed.dart (93%) rename packages/{stream_chat_flutter/lib/src/v4/channel_list_view => stream_chat_flutter_core/lib/src}/stream_channel_list_controller.dart (98%) rename packages/{stream_chat_flutter/lib/src/v4/channel_list_view => stream_chat_flutter_core/lib/src}/stream_channel_list_event_handler.dart (98%) diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart index b91684ed..5f3d7391 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; /// A [SafeArea] with an enabled toggle -class SimpleSafeArea extends StatefulWidget { +class SimpleSafeArea extends StatelessWidget { /// Constructor for [SimpleSafeArea] const SimpleSafeArea({ Key? key, @@ -15,17 +15,12 @@ class SimpleSafeArea extends StatefulWidget { /// Child widget to wrap final Widget child; - @override - _SimpleSafeAreaState createState() => _SimpleSafeAreaState(); -} - -class _SimpleSafeAreaState extends State { @override Widget build(BuildContext context) => SafeArea( - left: widget.enabled, - top: widget.enabled, - right: widget.enabled, - bottom: widget.enabled, - child: widget.child, + left: enabled, + top: enabled, + right: enabled, + bottom: enabled, + child: child, ); } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index adf69be0..1d88f354 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -1,11 +1,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 7e6dc769..83a31510 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -25,8 +25,6 @@ export 'src/mention_tile.dart'; export 'src/message_action.dart'; export 'src/message_input/countdown_button.dart'; export 'src/message_input/message_input.dart'; -export 'src/message_input/message_input_controller.dart'; -export 'src/message_input/message_text_field_controller.dart'; export 'src/message_input/stream_attachment_picker.dart'; export 'src/message_input/stream_message_send_button.dart'; export 'src/message_input/stream_message_text_field.dart'; @@ -55,8 +53,6 @@ export 'src/user_mention_tile.dart'; export 'src/utils.dart'; // v4 -export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; -export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart deleted file mode 100644 index 1b496784..00000000 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -void main() { - testWidgets( - 'should instantiate a new MessageInputController with empty message', - (tester) async { - final controller = MessageInputController()..text = 'test'; - - expect(controller.text, 'test'); - expect(controller.message.text, 'test'); - }, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart rename to packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 185c875c..65c6d8d8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -2,7 +2,9 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; /// A value listenable builder related to a [Message]. /// diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart similarity index 82% rename from packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart rename to packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart index 4a6c3708..0f9f75c6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/message_input/tld.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A function that takes a [BuildContext] and returns a [TextStyle]. typedef TextStyleBuilder = TextStyle? Function( @@ -33,17 +31,8 @@ class MessageTextFieldController extends TextEditingController { TextStyle? style, required bool withComposing, }) { - final pattern = textPatternStyle ?? - { - RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'): - (context, text) { - if (!text.split('.').last.isValidTLD()) return null; - return TextStyle( - color: MessageInputTheme.of(context).linkHighlightColor, - ); - }, - }; - if (pattern.isEmpty) { + final pattern = textPatternStyle; + if (pattern == null || pattern.isEmpty) { return super.buildTextSpan( context: context, style: style, diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/paged_value_notifier.dart rename to packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart similarity index 93% rename from packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart rename to packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart index 515a9742..a7ea0ea3 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart @@ -191,22 +191,20 @@ class _$Success extends Success @override bool operator ==(dynamic other) { return identical(this, other) || - (other is Success && - (identical(other.items, items) || - const DeepCollectionEquality().equals(other.items, items)) && - (identical(other.nextPageKey, nextPageKey) || - const DeepCollectionEquality() - .equals(other.nextPageKey, nextPageKey)) && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error))); + (other.runtimeType == runtimeType && + other is Success && + const DeepCollectionEquality().equals(other.items, items) && + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey) && + const DeepCollectionEquality().equals(other.error, error)); } @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(items) ^ - const DeepCollectionEquality().hash(nextPageKey) ^ - const DeepCollectionEquality().hash(error); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(items), + const DeepCollectionEquality().hash(nextPageKey), + const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override @@ -296,13 +294,13 @@ abstract class Success extends PagedValue { const Success._() : super._(); /// List with all items loaded so far. - List get items => throw _privateConstructorUsedError; + List get items; /// The key for the next page to be fetched. - Key? get nextPageKey => throw _privateConstructorUsedError; + Key? get nextPageKey; /// The current error, if any. - StreamChatError? get error => throw _privateConstructorUsedError; + StreamChatError? get error; @JsonKey(ignore: true) $SuccessCopyWith> get copyWith => throw _privateConstructorUsedError; @@ -347,7 +345,8 @@ class _$Loading extends Loading @override bool operator ==(dynamic other) { - return identical(this, other) || (other is Loading); + return identical(this, other) || + (other.runtimeType == runtimeType && other is Loading); } @override @@ -490,14 +489,14 @@ class _$Error extends Error @override bool operator ==(dynamic other) { return identical(this, other) || - (other is Error && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error))); + (other.runtimeType == runtimeType && + other is Error && + const DeepCollectionEquality().equals(other.error, error)); } @override int get hashCode => - runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); + Object.hash(runtimeType, const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override @@ -583,7 +582,7 @@ abstract class Error extends PagedValue { const factory Error(StreamChatError error) = _$Error; const Error._() : super._(); - StreamChatError get error => throw _privateConstructorUsedError; + StreamChatError get error; @JsonKey(ignore: true) $ErrorCopyWith> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart rename to packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index e7eb903e..7d3a281f 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'package:stream_chat/stream_chat.dart' hide Success; -import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart'; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart rename to packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index 44ace69e..8d548c67 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter_core/src/stream_channel_list_controller.dart'; /// Contains handlers that are called from [StreamChannelListController] for /// certain [Event]s. diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 8bd41c76..78d82d8b 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -7,10 +7,15 @@ export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.dart'; export 'src/lazy_load_scroll_view.dart'; +export 'src/message_input_controller.dart'; export 'src/message_list_core.dart' hide MessageListCoreState; export 'src/message_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; +export 'src/message_text_field_controller.dart'; +export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder; export 'src/stream_channel.dart'; +export 'src/stream_channel_list_controller.dart'; +export 'src/stream_channel_list_event_handler.dart'; export 'src/stream_chat_core.dart'; export 'src/typedef.dart'; export 'src/user_list_core.dart' hide UserListCoreState; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index fb9307c1..cf1206cc 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -14,14 +14,17 @@ dependencies: connectivity_plus: ^2.1.0 flutter: sdk: flutter + freezed_annotation: ^1.0.0 meta: ^1.3.0 rxdart: ^0.27.0 stream_chat: ^3.3.1 dev_dependencies: + build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 fake_async: ^1.2.0 flutter_test: sdk: flutter + freezed: ^1.0.0 mocktail: ^0.2.0 From 357357cdff63630d37fd108fb5c6b3ae00b64c32 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 8 Mar 2022 10:29:49 +0100 Subject: [PATCH 18/19] fix tests --- .../lib/src/theme/message_input_theme.dart | 4 +++- .../test/src/typing_indicator_test.dart | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 9bc8ffd2..81047d77 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -188,7 +190,7 @@ class MessageInputThemeData with Diagnosticable { linkHighlightColor: Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), enableSafeArea: a.enableSafeArea, - elevation: Tween(begin: a.elevation, end: b.elevation).transform(t), + elevation: lerpDouble(a.elevation, b.elevation, t), shadow: BoxShadow.lerp(a.shadow, b.shadow, t), ); diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 8d790266..0a98aff7 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -63,19 +63,23 @@ void main() { Event(type: EventType.typingStart), })); + const typingKey = Key('typing'); + await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, child: const Scaffold( - body: TypingIndicator(), + body: TypingIndicator( + key: typingKey, + ), ), ), ), )); - expect(find.byKey(const Key('typings')), findsOneWidget); + expect(find.byKey(typingKey), findsOneWidget); }, ); } From f8c0f808d81698ebea7cb9782ab1409aabc86b0f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 8 Mar 2022 10:44:52 +0100 Subject: [PATCH 19/19] add missing localizations --- .../lib/src/message_list_view.dart | 1 + .../lib/src/stream_chat_localizations.dart | 1 + .../lib/src/stream_chat_localizations_pt.dart | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 2773b192..e4d67740 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -526,6 +526,7 @@ class _MessageListViewState extends State { return ((index + 2) * 2) - 1; } } + return null; }, // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 0c7ce68e..6ea2eda4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -75,6 +75,7 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { case 'pt': return const StreamChatLocalizationsPt(); } + return null; } /// Implementation of localized strings for the stream chat widgets diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 44c02081..4c1d2a58 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -371,4 +371,15 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get slowModeOnLabel => 'Modo lento ativado'; + + @override + String get linkDisabledDetails => + 'O envio de links não é permitido nesta conversa.'; + + @override + String get linkDisabledError => 'Os links estão desativados'; + + @override + String get sendMessagePermissionError => + 'Você não tem permissão para enviar mensagens'; }