chore(ui): initial channel list view draft with controller.
Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
@@ -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<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Member> 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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Key, Value>
|
||||
= ValueListenableBuilder<PagedValue<Key, Value>>;
|
||||
|
||||
abstract class PagedValueNotifier<Key, Value>
|
||||
extends ValueNotifier<PagedValue<Key, Value>> {
|
||||
/// Creates a [PagedValueNotifier]
|
||||
PagedValueNotifier(this._initialValue) : super(_initialValue);
|
||||
|
||||
/// Stores initialValue in case we need to call [refresh].
|
||||
final PagedValue<Key, Value> _initialValue;
|
||||
|
||||
/// Retry any failed load requests.
|
||||
///
|
||||
/// Unlike [refresh], this does not resets the whole [value],
|
||||
/// it only retries the last failed load request.
|
||||
Future<void> retry() {
|
||||
var lastValue = value;
|
||||
assert(lastValue.hasError, '');
|
||||
lastValue = lastValue as Success<Key, Value>;
|
||||
|
||||
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<void> refresh() {
|
||||
value = _initialValue;
|
||||
return doInitialLoad();
|
||||
}
|
||||
|
||||
/// Load initial data from the server.
|
||||
Future<void> doInitialLoad();
|
||||
|
||||
/// Load more data from the server using [nextPageKey].
|
||||
Future<void> loadMore(Key nextPageKey);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PagedValue<Key, Value> with _$PagedValue<Key, Value> {
|
||||
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<Value> items,
|
||||
|
||||
/// The key for the next page to be fetched.
|
||||
Key? nextPageKey,
|
||||
|
||||
/// The current error, if any.
|
||||
StreamChatError? error,
|
||||
}) = Success<Key, Value>;
|
||||
|
||||
bool get hasNextPage {
|
||||
assert(this is Success<Key, Value>, '');
|
||||
return (this as Success<Key, Value>).nextPageKey != null;
|
||||
}
|
||||
|
||||
bool get hasError {
|
||||
assert(this is Success<Key, Value>, '');
|
||||
return (this as Success<Key, Value>).error != null;
|
||||
}
|
||||
|
||||
int get itemCount {
|
||||
assert(this is Success<Key, Value>, '');
|
||||
final count = (this as Success<Key, Value>).items.length;
|
||||
if (hasNextPage || hasError) return count + 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
const factory PagedValue.loading() = Loading;
|
||||
|
||||
const factory PagedValue.error(StreamChatError error) = Error;
|
||||
}
|
||||
@@ -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>(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<Key, Value> call<Key, Value>(
|
||||
{required List<Value> items, Key? nextPageKey, StreamChatError? error}) {
|
||||
return Success<Key, Value>(
|
||||
items: items,
|
||||
nextPageKey: nextPageKey,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
Loading<Key, Value> loading<Key, Value>() {
|
||||
return Loading<Key, Value>();
|
||||
}
|
||||
|
||||
Error<Key, Value> error<Key, Value>(StreamChatError error) {
|
||||
return Error<Key, Value>(
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $PagedValue = _$PagedValueTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PagedValue<Key, Value> {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)
|
||||
$default, {
|
||||
required TResult Function() loading,
|
||||
required TResult Function(StreamChatError error) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)?
|
||||
$default, {
|
||||
TResult Function()? loading,
|
||||
TResult Function(StreamChatError error)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)?
|
||||
$default, {
|
||||
TResult Function()? loading,
|
||||
TResult Function(StreamChatError error)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value) $default, {
|
||||
required TResult Function(Loading<Key, Value> value) loading,
|
||||
required TResult Function(Error<Key, Value> value) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $PagedValueCopyWith<Key, Value, $Res> {
|
||||
factory $PagedValueCopyWith(PagedValue<Key, Value> value,
|
||||
$Res Function(PagedValue<Key, Value>) then) =
|
||||
_$PagedValueCopyWithImpl<Key, Value, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PagedValueCopyWithImpl<Key, Value, $Res>
|
||||
implements $PagedValueCopyWith<Key, Value, $Res> {
|
||||
_$PagedValueCopyWithImpl(this._value, this._then);
|
||||
|
||||
final PagedValue<Key, Value> _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(PagedValue<Key, Value>) _then;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SuccessCopyWith<Key, Value, $Res> {
|
||||
factory $SuccessCopyWith(
|
||||
Success<Key, Value> value, $Res Function(Success<Key, Value>) then) =
|
||||
_$SuccessCopyWithImpl<Key, Value, $Res>;
|
||||
$Res call({List<Value> items, Key? nextPageKey, StreamChatError? error});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SuccessCopyWithImpl<Key, Value, $Res>
|
||||
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
|
||||
implements $SuccessCopyWith<Key, Value, $Res> {
|
||||
_$SuccessCopyWithImpl(
|
||||
Success<Key, Value> _value, $Res Function(Success<Key, Value>) _then)
|
||||
: super(_value, (v) => _then(v as Success<Key, Value>));
|
||||
|
||||
@override
|
||||
Success<Key, Value> get _value => super._value as Success<Key, Value>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? items = freezed,
|
||||
Object? nextPageKey = freezed,
|
||||
Object? error = freezed,
|
||||
}) {
|
||||
return _then(Success<Key, Value>(
|
||||
items: items == freezed
|
||||
? _value.items
|
||||
: items // ignore: cast_nullable_to_non_nullable
|
||||
as List<Value>,
|
||||
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<Key, Value> extends Success<Key, Value>
|
||||
with DiagnosticableTreeMixin {
|
||||
const _$Success({required this.items, this.nextPageKey, this.error})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
|
||||
/// List with all items loaded so far.
|
||||
final List<Value> 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<Key, Value> &&
|
||||
(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<Key, Value, Success<Key, Value>> get copyWith =>
|
||||
_$SuccessCopyWithImpl<Key, Value, Success<Key, Value>>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(Success<Key, Value> value) $default, {
|
||||
required TResult Function(Loading<Key, Value> value) loading,
|
||||
required TResult Function(Error<Key, Value> value) error,
|
||||
}) {
|
||||
return $default(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
}) {
|
||||
return $default?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if ($default != null) {
|
||||
return $default(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Success<Key, Value> extends PagedValue<Key, Value> {
|
||||
const factory Success(
|
||||
{required List<Value> items,
|
||||
Key? nextPageKey,
|
||||
StreamChatError? error}) = _$Success<Key, Value>;
|
||||
const Success._() : super._();
|
||||
|
||||
/// List with all items loaded so far.
|
||||
List<Value> 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<Key, Value, Success<Key, Value>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LoadingCopyWith<Key, Value, $Res> {
|
||||
factory $LoadingCopyWith(
|
||||
Loading<Key, Value> value, $Res Function(Loading<Key, Value>) then) =
|
||||
_$LoadingCopyWithImpl<Key, Value, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoadingCopyWithImpl<Key, Value, $Res>
|
||||
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
|
||||
implements $LoadingCopyWith<Key, Value, $Res> {
|
||||
_$LoadingCopyWithImpl(
|
||||
Loading<Key, Value> _value, $Res Function(Loading<Key, Value>) _then)
|
||||
: super(_value, (v) => _then(v as Loading<Key, Value>));
|
||||
|
||||
@override
|
||||
Loading<Key, Value> get _value => super._value as Loading<Key, Value>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$Loading<Key, Value> extends Loading<Key, Value>
|
||||
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<Key, Value>);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)
|
||||
$default, {
|
||||
required TResult Function() loading,
|
||||
required TResult Function(StreamChatError error) error,
|
||||
}) {
|
||||
return loading();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)?
|
||||
$default, {
|
||||
TResult Function()? loading,
|
||||
TResult Function(StreamChatError error)? error,
|
||||
}) {
|
||||
return loading?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(Success<Key, Value> value) $default, {
|
||||
required TResult Function(Loading<Key, Value> value) loading,
|
||||
required TResult Function(Error<Key, Value> value) error,
|
||||
}) {
|
||||
return loading(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
}) {
|
||||
return loading?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (loading != null) {
|
||||
return loading(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Loading<Key, Value> extends PagedValue<Key, Value> {
|
||||
const factory Loading() = _$Loading<Key, Value>;
|
||||
const Loading._() : super._();
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ErrorCopyWith<Key, Value, $Res> {
|
||||
factory $ErrorCopyWith(
|
||||
Error<Key, Value> value, $Res Function(Error<Key, Value>) then) =
|
||||
_$ErrorCopyWithImpl<Key, Value, $Res>;
|
||||
$Res call({StreamChatError error});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ErrorCopyWithImpl<Key, Value, $Res>
|
||||
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
|
||||
implements $ErrorCopyWith<Key, Value, $Res> {
|
||||
_$ErrorCopyWithImpl(
|
||||
Error<Key, Value> _value, $Res Function(Error<Key, Value>) _then)
|
||||
: super(_value, (v) => _then(v as Error<Key, Value>));
|
||||
|
||||
@override
|
||||
Error<Key, Value> get _value => super._value as Error<Key, Value>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? error = freezed,
|
||||
}) {
|
||||
return _then(Error<Key, Value>(
|
||||
error == freezed
|
||||
? _value.error
|
||||
: error // ignore: cast_nullable_to_non_nullable
|
||||
as StreamChatError,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$Error<Key, Value> extends Error<Key, Value>
|
||||
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<Key, Value> &&
|
||||
(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<Key, Value, Error<Key, Value>> get copyWith =>
|
||||
_$ErrorCopyWithImpl<Key, Value, Error<Key, Value>>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> items, Key? nextPageKey, StreamChatError? error)?
|
||||
$default, {
|
||||
TResult Function()? loading,
|
||||
TResult Function(StreamChatError error)? error,
|
||||
}) {
|
||||
return error?.call(this.error);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<Value> 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 extends Object?>(
|
||||
TResult Function(Success<Key, Value> value) $default, {
|
||||
required TResult Function(Loading<Key, Value> value) loading,
|
||||
required TResult Function(Error<Key, Value> value) error,
|
||||
}) {
|
||||
return error(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
}) {
|
||||
return error?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(Success<Key, Value> value)? $default, {
|
||||
TResult Function(Loading<Key, Value> value)? loading,
|
||||
TResult Function(Error<Key, Value> value)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (error != null) {
|
||||
return error(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Error<Key, Value> extends PagedValue<Key, Value> {
|
||||
const factory Error(StreamChatError error) = _$Error<Key, Value>;
|
||||
const Error._() : super._();
|
||||
|
||||
StreamChatError get error => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$ErrorCopyWith<Key, Value, Error<Key, Value>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+91
@@ -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<int, Channel> {
|
||||
/// 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<int, Channel> 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<SortOption<ChannelModel>>? 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<void> 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<void> loadMore(int nextPageKey) async {
|
||||
assert(value is Success<int, Channel>, '');
|
||||
final previousValue = value as Success<int, Channel>;
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+379
@@ -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<bool>(
|
||||
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<List<Member>>(
|
||||
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<List<Message>>(
|
||||
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<DateTime>(
|
||||
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: <Widget>[
|
||||
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<List<Message>>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+369
@@ -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<StreamChannelListView> {
|
||||
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<int, Channel>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// 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<String>(
|
||||
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<User>(
|
||||
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<Member>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String>(
|
||||
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<Member> 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 = <Member>[];
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user