chore: move controllers from ui to core

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2022-02-07 15:25:07 +05:30
committed by xsahil03x
parent ce22e854a8
commit 202af02f8e
12 changed files with 44 additions and 71 deletions
@@ -1,307 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A value listenable builder related to a [Message].
///
/// Pass in a [MessageInputController] as the `valueListenable`.
typedef MessageValueListenableBuilder = ValueListenableBuilder<Message>;
/// Controller for storing and mutating a [Message] value.
class MessageInputController extends ValueNotifier<Message> {
/// Creates a controller for an editable text field.
///
/// This constructor treats a null [message] argument as if it were the empty
/// message.
factory MessageInputController({
Message? message,
Map<RegExp, TextStyleBuilder>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: message ?? Message(),
textPatternStyle: textPatternStyle,
);
/// Creates a controller for an editable text field from an initial [text].
factory MessageInputController.fromText(
String? text, {
Map<RegExp, TextStyleBuilder>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: Message(text: text),
textPatternStyle: textPatternStyle,
);
/// Creates a controller for an editable text field from initial
/// [attachments].
factory MessageInputController.fromAttachments(
List<Attachment> attachments, {
Map<RegExp, TextStyleBuilder>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: Message(attachments: attachments),
textPatternStyle: textPatternStyle,
);
MessageInputController._({
required Message initialMessage,
Map<RegExp, TextStyleBuilder>? textPatternStyle,
}) : _textEditingController = MessageTextFieldController.fromValue(
initialMessage.text == null
? const TextEditingValue()
: TextEditingValue(
text: initialMessage.text!,
composing: TextRange.collapsed(initialMessage.text!.length),
),
textPatternStyle: textPatternStyle,
),
_initialMessage = initialMessage,
super(initialMessage) {
addListener(_textEditingSyncer);
}
void _textEditingSyncer() {
final cleanText = value.command == null
? value.text
: value.text?.replaceFirst('/${value.command} ', '');
if (cleanText != _textEditingController.text) {
final previousOffset = _textEditingController.value.selection.start;
final previousText = _textEditingController.text;
final diff = (cleanText?.length ?? 0) - previousText.length;
_textEditingController
..text = cleanText ?? ''
..selection = TextSelection.collapsed(
offset: previousOffset + diff,
);
}
}
/// Returns the current message associated with this controller.
Message get message => value;
/// Returns the controller of the text field linked to this controller.
MessageTextFieldController get textEditingController =>
_textEditingController;
final MessageTextFieldController _textEditingController;
/// Returns the text of the message.
String get text => _textEditingController.text;
Message _initialMessage;
/// Sets the message.
set message(Message message) {
value = message;
}
/// Sets the message that's being quoted.
set quotedMessage(Message message) {
value = value.copyWith(
quotedMessage: message,
quotedMessageId: message.id,
);
}
/// Clears the quoted message.
void clearQuotedMessage() {
value = value.copyWith(
quotedMessageId: null,
quotedMessage: null,
);
}
/// Sets a command for the message.
set command(Command command) {
value = value.copyWith(
command: command.name,
text: '/${command.name} ',
);
}
/// Sets the text of the message.
set text(String newText) {
var newTextWithCommand = newText;
if (value.command != null) {
if (!newText.startsWith('/${value.command}')) {
newTextWithCommand = '/${value.command} $newText';
}
}
value = value.copyWith(text: newTextWithCommand);
}
/// Returns the baseOffset of the text field.
int get baseOffset => textEditingController.selection.baseOffset;
/// Returns the start of the selection of the text field.
int get selectionStart => textEditingController.selection.start;
/// Sets the [showInChannel] flag of the message.
set showInChannel(bool newValue) {
value = value.copyWith(showInChannel: newValue);
}
/// Returns true if the message is in a thread and
/// should be shown in the main channel as well.
bool get showInChannel => value.showInChannel ?? false;
/// Returns the attachments of the message.
List<Attachment> get attachments => value.attachments;
/// Sets the list of [attachments] for the message.
set attachments(List<Attachment> attachments) {
value = value.copyWith(attachments: attachments);
}
/// Adds a new attachment to the message.
void addAttachment(Attachment attachment) {
attachments = [...attachments, attachment];
}
/// Adds a new attachment at the specified [index].
void addAttachmentAt(int index, Attachment attachment) {
attachments = [...attachments]..insert(index, attachment);
}
/// Removes the specified [attachment] from the message.
void removeAttachment(Attachment attachment) {
attachments = [...attachments]..remove(attachment);
}
/// Remove the attachment with the given [attachmentId].
void removeAttachmentById(String attachmentId) {
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId);
}
/// Removes the attachment at the given [index].
void removeAttachmentAt(int index) {
attachments = [...attachments]..removeAt(index);
}
/// Clears the message attachments.
void clearAttachments() {
attachments = [];
}
// Only used to store the value locally in order to remove it if we call
// [clearOGAttachment] or [setOGAttachment] again.
Attachment? _ogAttachment;
/// Returns the og attachment of the message if set
Attachment? get ogAttachment =>
attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id);
/// Sets the og attachment in the message.
void setOGAttachment(Attachment attachment) {
attachments = [...attachments]
..remove(_ogAttachment)
..insert(0, attachment);
_ogAttachment = attachment;
}
/// Removes the og attachment.
void clearOGAttachment() {
if (_ogAttachment != null) {
removeAttachment(_ogAttachment!);
}
_ogAttachment = null;
}
/// Returns the list of mentioned users in the message.
List<User> get mentionedUsers => value.mentionedUsers;
/// Sets the mentioned users.
set mentionedUsers(List<User> users) {
value = value.copyWith(mentionedUsers: users);
}
/// Adds a user to the list of mentioned users.
void addMentionedUser(User user) {
mentionedUsers = [...mentionedUsers, user];
}
/// Removes the specified [user] from the mentioned users list.
void removeMentionedUser(User user) {
mentionedUsers = [...mentionedUsers]..remove(user);
}
/// Removes the mentioned user with the given [userId].
void removeMentionedUserById(String userId) {
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId);
}
/// Removes all mentioned users from the message.
void clearMentionedUsers() {
mentionedUsers = [];
}
/// Sets the [message], or [value], to empty.
///
/// After calling this function, [text], [attachments] and [mentionedUsers]
/// will all be empty.
///
/// Calling this will notify all the listeners of this
/// [MessageInputController] that they need to update
/// (calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
value = Message();
_textEditingController.clear();
}
/// Sets the [value] to the initial [Message] value.
void reset({bool resetId = true}) {
if (resetId) {
final newId = const Uuid().v4();
_initialMessage = _initialMessage.copyWith(id: newId);
}
value = _initialMessage;
}
@override
void dispose() {
removeListener(_textEditingSyncer);
_textEditingController.dispose();
super.dispose();
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [MessageInputController].
///
/// The [MessageInputController] is accessible via the [value] getter. During
/// state restoration, the property will restore [MessageInputController.value]
/// to the value it had when the restoration data it is getting restored from
/// was collected.
class RestorableMessageInputController
extends RestorableChangeNotifier<MessageInputController> {
/// Creates a [RestorableMessageInputController].
///
/// This constructor creates a default [Message] when no `message` argument
/// is supplied.
RestorableMessageInputController({Message? message})
: _initialValue = message ?? Message();
/// Creates a [RestorableMessageInputController] from an initial
/// [text] value.
factory RestorableMessageInputController.fromText(String? text) =>
RestorableMessageInputController(message: Message(text: text));
final Message _initialValue;
@override
MessageInputController createDefaultValue() =>
MessageInputController(message: _initialValue);
@override
MessageInputController fromPrimitives(Object? data) {
final message = Message.fromJson(json.decode(data! as String));
return MessageInputController(message: message);
}
@override
String toPrimitives() => json.encode(value.value);
}
@@ -1,97 +0,0 @@
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(
BuildContext context,
String text,
);
/// Controller for the [StreamTextField] widget.
class MessageTextFieldController extends TextEditingController {
/// Returns a new MessageTextFieldController
MessageTextFieldController({
String? text,
this.textPatternStyle,
}) : super(text: text);
/// Returns a new MessageTextFieldController with the given text [value].
MessageTextFieldController.fromValue(
TextEditingValue? value, {
this.textPatternStyle,
}) : super.fromValue(value);
/// A map of style to apply to the text matching the RegExp patterns.
final Map<RegExp, TextStyleBuilder>? textPatternStyle;
/// Builds a [TextSpan] from the current text,
/// highlighting the matches for [textPatternStyle].
@override
TextSpan buildTextSpan({
required BuildContext context,
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) {
return super.buildTextSpan(
context: context,
style: style,
withComposing: withComposing,
);
}
return TextSpan(text: text, style: style).splitMapJoin(
RegExp(pattern.keys.map((it) => it.pattern).join('|')),
onMatch: (match) {
final text = match[0]!;
final key = pattern.keys.firstWhere((it) => it.hasMatch(text));
return TextSpan(
text: text,
style: pattern[key]?.call(
context,
text,
),
);
},
);
}
}
extension _TextSpanX on TextSpan {
TextSpan splitMapJoin(
Pattern pattern, {
TextSpan Function(Match)? onMatch,
TextSpan Function(TextSpan)? onNonMatch,
}) {
final children = <TextSpan>[];
toPlainText().splitMapJoin(
pattern,
onMatch: (match) {
final span = TextSpan(text: match.group(0), style: style);
final updated = onMatch?.call(match);
children.add(updated ?? span);
return span.toPlainText();
},
onNonMatch: (text) {
final span = TextSpan(text: text, style: style);
final updatedSpan = onNonMatch?.call(span);
children.add(updatedSpan ?? span);
return span.toPlainText();
},
);
return TextSpan(style: style, children: children);
}
}
@@ -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<SimpleSafeArea> {
@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,
);
}
@@ -1,136 +0,0 @@
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';
/// Default initial page size multiplier.
const defaultInitialPagedLimitMultiplier = 3;
/// Value listenable for paged data.
typedef PagedValueListenableBuilder<Key, Value>
= ValueListenableBuilder<PagedValue<Key, Value>>;
/// 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<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;
/// Returns the currently loaded items
List<Value> get currentItems => value.asSuccess.items;
/// Appends [newItems] to the previously loaded ones and replaces
/// the next page's key.
void appendPage({
required List<Value> 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<Value> 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<void> retry() {
final lastValue = value.asSuccess;
assert(lastValue.hasError, '');
final nextPageKey = lastValue.nextPageKey;
// resetting the error
value = lastValue.copyWith(error: null);
// ignore: null_check_on_nullable_type_parameter
return loadMore(nextPageKey!);
}
/// 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<void> refresh({bool resetValue = true}) {
if (resetValue) 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);
}
/// Paged value that can be used with [PagedValueNotifier].
@freezed
abstract class PagedValue<Key, Value> with _$PagedValue<Key, Value> {
/// Represents the success state of the [PagedValue]
// @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>;
const PagedValue._();
/// 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<Key, Value>;
/// Returns the [PagedValue] as [Success].
Success<Key, Value> get asSuccess {
assert(
isSuccess,
'Cannot get asSuccess if the PagedValue is not in the Success state',
);
return this as Success<Key, Value>;
}
/// 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 {
final count = asSuccess.items.length;
if (hasNextPage || hasError) return count + 1;
return count;
}
}
@@ -1,590 +0,0 @@
// 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;
}
@@ -1,281 +0,0 @@
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';
/// 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:
/// * 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.
/// * Pause and Resume all subscriptions added to this composite.
class StreamChannelListController extends PagedValueNotifier<int, Channel> {
/// Creates a Stream channel list controller.
///
/// * `client` is the Stream chat client to use for the channels list.
///
/// * `channelEventHandlers` is the channel events to use for the channels
/// list. This class can be mixed in or extended to create custom overrides.
/// See [StreamChannelListEventHandler] 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,
StreamChannelListEventHandler? eventHandler,
this.filter,
this.sort,
this.presence = true,
this.limit = defaultChannelPagedLimit,
this.messageLimit,
this.memberLimit,
}) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(),
super(const PagedValue.loading());
/// Creates a [StreamChannelListController] from the passed [value].
StreamChannelListController.fromValue(
PagedValue<int, Channel> value, {
required this.client,
StreamChannelListEventHandler? eventHandler,
this.filter,
this.sort,
this.presence = true,
this.limit = defaultChannelPagedLimit,
this.messageLimit,
this.memberLimit,
}) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(),
super(value);
/// 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<SortOption<ChannelModel>>? sort;
/// If true youll receive user presence updates via the websocket events
final bool presence;
/// The limit to apply to the channel list. The default is set to
/// [defaultChannelPagedLimit].
final int limit;
/// Number of messages to fetch in each channel.
final int? messageLimit;
/// Number of members to fetch in each channel.
final int? memberLimit;
@override
Future<void> doInitialLoad() async {
final limit = min(
this.limit * defaultInitialPagedLimitMultiplier,
_kDefaultBackendPaginationLimit,
);
try {
await for (final channels in client.queryChannels(
filter: filter,
sort: sort,
memberLimit: memberLimit,
messageLimit: messageLimit,
presence: presence,
paginationParams: PaginationParams(limit: limit),
)) {
final nextKey = channels.length < limit ? null : channels.length;
value = PagedValue(
items: channels,
nextPageKey: nextKey,
);
}
// start listening to events
_subscribeToChannelListEvents();
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = PagedValue.error(chatError);
}
}
@override
Future<void> loadMore(int nextPageKey) async {
final previousValue = value.asSuccess;
try {
await for (final channels in client.queryChannels(
filter: filter,
sort: sort,
memberLimit: memberLimit,
messageLimit: messageLimit,
presence: presence,
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,
);
}
} on StreamChatError catch (error) {
value = previousValue.copyWith(error: error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = previousValue.copyWith(error: chatError);
}
}
/// Replaces the previously loaded channels with [channels] and updates
/// the nextPageKey.
set channels(List<Channel> channels) {
value = PagedValue(
items: channels,
nextPageKey: channels.length,
);
}
/// Returns/Creates a new Channel and starts watching it.
Future<Channel> getChannel({
required String id,
required String type,
}) async {
final channel = client.channel(type, id: id);
await channel.watch();
return channel;
}
/// Leaves the [channel] and updates the list.
Future<void> 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<void> deleteChannel(Channel channel) async {
await channel.delete();
}
/// Mutes the [channel] and updates the list.
Future<void> muteChannel(Channel channel) async {
await channel.mute();
}
/// Un-mutes the [channel] and updates the list.
Future<void> unmuteChannel(Channel channel) async {
await channel.unmute();
}
/// 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<Event>? _channelEventSubscription;
// Subscribes to the channel list events.
void _subscribeToChannelListEvents() {
if (_channelEventSubscription != null) {
_unsubscribeFromChannelListEvents();
}
_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);
} else if (eventType == EventType.channelHidden) {
_eventHandler.onChannelHidden(event, this);
} else if (eventType == EventType.channelTruncated) {
_eventHandler.onChannelTruncated(event, this);
} else if (eventType == EventType.channelUpdated) {
_eventHandler.onChannelUpdated(event, this);
} else if (eventType == EventType.channelVisible) {
_eventHandler.onChannelVisible(event, this);
} else if (eventType == EventType.connectionRecovered) {
_eventHandler.onConnectionRecovered(event, this);
} else if (eventType == EventType.connectionChanged) {
if (event.online != null) {
_eventHandler.onConnectionRecovered(event, this);
}
} else if (eventType == EventType.messageNew) {
_eventHandler.onMessageNew(event, this);
} else if (eventType == EventType.notificationAddedToChannel) {
_eventHandler.onNotificationAddedToChannel(event, this);
} else if (eventType == EventType.notificationMessageNew) {
_eventHandler.onNotificationMessageNew(event, this);
} else if (eventType == EventType.notificationRemovedFromChannel) {
_eventHandler.onNotificationRemovedFromChannel(event, this);
} else if (eventType == 'user.presence.changed' ||
eventType == EventType.userUpdated) {
_eventHandler.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<void>? resumeSignal]) {
_channelEventSubscription?.pause(resumeSignal);
}
/// Resumes all subscriptions added to this composite.
void resumeEventsSubscription() {
_channelEventSubscription?.resume();
}
@override
void dispose() {
_unsubscribeFromChannelListEvents();
super.dispose();
}
}
@@ -1,213 +0,0 @@
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
/// certain [Event]s.
///
/// 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),
);
controller.channels = updatedChannels;
}
/// 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);
}
/// 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();
}
/// 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 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;
}
/// 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;
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];
}
}
@@ -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';
@@ -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';
@@ -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');
},
);
}