Merge pull request #412 from GetStream/feat/ui-linter

feat: added lint changes, started migrating
This commit is contained in:
Salvatore Giordano
2021-05-06 12:03:22 +02:00
committed by GitHub
94 changed files with 6570 additions and 6638 deletions
@@ -1,12 +1,18 @@
analyzer: analyzer:
enable-experiment:
- extension-methods
exclude: exclude:
- lib/**/*.g.dart - lib/**/*.g.dart
- example/**
- lib/src/emoji
- lib/**/*.freezed.dart - lib/**/*.freezed.dart
- example/* - test/**
- test/*
linter: linter:
rules: rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
- always_use_package_imports - always_use_package_imports
- avoid_empty_else - avoid_empty_else
- avoid_relative_lib_imports - avoid_relative_lib_imports
@@ -43,7 +49,6 @@ linter:
- avoid_private_typedef_functions - avoid_private_typedef_functions
- avoid_redundant_argument_values - avoid_redundant_argument_values
- avoid_return_types_on_setters - avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void - avoid_returning_null_for_void
- avoid_shadowing_type_parameters - avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements - avoid_single_cascade_in_expression_statements
@@ -139,8 +144,7 @@ linter:
- package_names - package_names
- sort_pub_dependencies - sort_pub_dependencies
# To be added when null-safe: - cast_nullable_to_non_nullable
# - cast_nullable_to_non_nullable - unnecessary_null_checks
#- unnecessary_null_checks - tighten_type_of_initializing_formals
# - tighten_type_of_initializing_formals - null_check_on_nullable_type_parameter
# - null_check_on_nullable_type_parameter
-143
View File
@@ -1,143 +0,0 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
@@ -646,7 +646,7 @@ class Channel {
messageId: messageId, messageId: messageId,
createdAt: now, createdAt: now,
type: type, type: type,
user: user!, user: user,
score: 1, score: 1,
extraData: extraData, extraData: extraData,
); );
@@ -654,7 +654,7 @@ class Channel {
// Inserting at the 0th index as it's the latest reaction // Inserting at the 0th index as it's the latest reaction
latestReactions.insert(0, newReaction); latestReactions.insert(0, newReaction);
final ownReactions = [...latestReactions] final ownReactions = [...latestReactions]
..removeWhere((it) => it.userId != user.id); ..removeWhere((it) => it.userId != user!.id);
final newMessage = message.copyWith( final newMessage = message.copyWith(
reactionCounts: {...message.reactionCounts ?? <String, int>{}} reactionCounts: {...message.reactionCounts ?? <String, int>{}}
+1 -2
View File
@@ -1495,8 +1495,7 @@ class ClientState {
Stream<OwnUser?> get userStream => _userController.stream; Stream<OwnUser?> get userStream => _userController.stream;
/// The current user /// The current user
Map<String?, User?> get users => Map<String?, User?> get users => _usersController.value!;
_usersController.value as Map<String?, User?>;
/// The current user as a stream /// The current user as a stream
Stream<Map<String?, User?>> get usersStream => _usersController.stream; Stream<Map<String?, User?>> get usersStream => _usersController.stream;
@@ -78,10 +78,15 @@ abstract class ChatPersistenceClient {
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination), getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
]); ]);
return ChannelState( return ChannelState(
// ignore: cast_nullable_to_non_nullable
members: data[0] as List<Member>, members: data[0] as List<Member>,
// ignore: cast_nullable_to_non_nullable
read: data[1] as List<Read>, read: data[1] as List<Read>,
// ignore: cast_nullable_to_non_nullable
channel: data[2] as ChannelModel?, channel: data[2] as ChannelModel?,
// ignore: cast_nullable_to_non_nullable
messages: data[3] as List<Message>, messages: data[3] as List<Message>,
// ignore: cast_nullable_to_non_nullable
pinnedMessages: data[4] as List<Message>, pinnedMessages: data[4] as List<Message>,
); );
} }
@@ -112,7 +112,7 @@ class ChannelModel {
/// Shortcut for channel name /// Shortcut for channel name
String get name => String get name =>
extraData.containsKey('name') ? extraData['name'] as String : cid; extraData.containsKey('name') ? extraData['name']! as String : cid;
/// Serialize to json /// Serialize to json
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot( Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
@@ -83,7 +83,7 @@ class User {
/// Shortcut for user name /// Shortcut for user name
String get name { String get name {
if (extraData.containsKey('name')) { if (extraData.containsKey('name')) {
final name = extraData['name'] as String; final name = extraData['name']! as String;
if (name.isNotEmpty) return name; if (name.isNotEmpty) return name;
} }
return id; return id;
@@ -1,64 +0,0 @@
include: package:pedantic/analysis_options.yaml
analyzer:
enable-experiment:
- extension-methods
exclude:
- lib/**/*.g.dart
- example/**
- lib/src/emoji
linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
# - always_declare_return_types
# - always_specify_types
# - annotate_overrides
# - avoid_as
- avoid_empty_else
- avoid_init_to_null
- avoid_return_types_on_setters
- avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
# - comment_references # we do not presume as to what people want to reference in their dartdocs
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
# - invariant_booleans
# - iterable_contains_unrelated_type
- library_names
# - library_prefixes
# - list_remove_unrelated_type
# - literal_only_boolean_expressions
- non_constant_identifier_names
# - one_member_abstracts
# - only_throw_errors
# - overridden_fields
# - package_api_docs
- package_names
- package_prefixed_library_names
- prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
# - public_member_api_docs
- slash_for_doc_comments
# - sort_constructors_first
# - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2
- test_types_in_equals
- throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types
- type_init_formals
# - unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
@@ -58,7 +58,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user.id], [StreamChat.of(context).user!.id],
), ),
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
@@ -1,7 +1,7 @@
export 'attachment_upload_state_builder.dart';
export 'attachment_widget.dart'
show AttachmentError, AttachmentSource, AttachmentSourceX;
export 'file_attachment.dart'; export 'file_attachment.dart';
export 'giphy_attachment.dart'; export 'giphy_attachment.dart';
export 'image_attachment.dart'; export 'image_attachment.dart';
export 'video_attachment.dart'; export 'video_attachment.dart';
export 'attachment_upload_state_builder.dart';
export 'attachment_widget.dart'
show AttachmentError, AttachmentSource, AttachmentSourceX;
@@ -1,29 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_theme.dart'; /// Title for attachments
import '../utils.dart';
class AttachmentTitle extends StatelessWidget { class AttachmentTitle extends StatelessWidget {
/// Supply attachment and theme for constructing title
const AttachmentTitle({ const AttachmentTitle({
Key? key, Key? key,
required this.attachment, required this.attachment,
required this.messageTheme, required this.messageTheme,
}) : super(key: key); }) : super(key: key);
/// Theme to apply to text
final MessageTheme messageTheme; final MessageTheme messageTheme;
/// Attachment data to display
final Attachment attachment; final Attachment attachment;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector(
onTap: () { onTap: () {
if (attachment.titleLink != null) { if (attachment.titleLink != null) {
launchURL(context, attachment.titleLink!); launchURL(context, attachment.titleLink);
} }
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -37,7 +39,8 @@ class AttachmentTitle extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null) if (attachment.titleLink != null ||
attachment.ogScrapeUrl != null)
Text( Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!) Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!)
.authority .authority
@@ -54,4 +57,3 @@ class AttachmentTitle extends StatelessWidget {
), ),
); );
} }
}
@@ -2,17 +2,15 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget to build in progress
typedef InProgressBuilder = Widget Function(BuildContext, int, int); typedef InProgressBuilder = Widget Function(BuildContext, int, int);
/// Widget to build on failure
typedef FailedBuilder = Widget Function(BuildContext, String); typedef FailedBuilder = Widget Function(BuildContext, String);
/// Widget to display attachment upload state
class AttachmentUploadStateBuilder extends StatelessWidget { class AttachmentUploadStateBuilder extends StatelessWidget {
final Message message; /// Constructor for creating an [AttachmentUploadStateBuilder] widget
final Attachment attachment;
final FailedBuilder? failedBuilder;
final WidgetBuilder? successBuilder;
final InProgressBuilder? inProgressBuilder;
final WidgetBuilder? preparingBuilder;
const AttachmentUploadStateBuilder({ const AttachmentUploadStateBuilder({
Key? key, Key? key,
required this.message, required this.message,
@@ -23,32 +21,46 @@ class AttachmentUploadStateBuilder extends StatelessWidget {
this.preparingBuilder, this.preparingBuilder,
}) : super(key: key); }) : super(key: key);
/// Message which attachment is added to
final Message message;
/// Attachment in concern
final Attachment attachment;
/// Widget to display when failed
final FailedBuilder? failedBuilder;
/// Widget to display when succeeded
final WidgetBuilder? successBuilder;
/// Widget to display when in progress
final InProgressBuilder? inProgressBuilder;
/// Widget to display when in prep
final WidgetBuilder? preparingBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message.status == MessageSendingStatus.sent) { if (message.status == MessageSendingStatus.sent) {
return Offstage(); return const Offstage();
} }
final messageId = message.id; final messageId = message.id;
final attachmentId = attachment.id; final attachmentId = attachment.id;
final inProgress = inProgressBuilder ?? final inProgress = inProgressBuilder ??
(context, int sent, int total) { (context, int sent, int total) => _InProgressState(
return _InProgressState(
sent: sent, sent: sent,
total: total, total: total,
attachmentId: attachmentId, attachmentId: attachmentId,
); );
};
final failed = failedBuilder ?? final failed = failedBuilder ??
(context, error) { (context, error) => _FailedState(
return _FailedState(
error: error, error: error,
messageId: messageId, messageId: messageId,
attachmentId: attachmentId, attachmentId: attachmentId,
); );
};
final success = successBuilder ?? (context) => _SuccessState(); final success = successBuilder ?? (context) => _SuccessState();
@@ -65,11 +77,6 @@ class AttachmentUploadStateBuilder extends StatelessWidget {
} }
class _IconButton extends StatelessWidget { class _IconButton extends StatelessWidget {
final Widget? icon;
final double iconSize;
final VoidCallback? onPressed;
final Color? fillColor;
const _IconButton({ const _IconButton({
Key? key, Key? key,
this.icon, this.icon,
@@ -78,16 +85,19 @@ class _IconButton extends StatelessWidget {
this.fillColor, this.fillColor,
}) : super(key: key); }) : super(key: key);
final Widget? icon;
final double iconSize;
final VoidCallback? onPressed;
final Color? fillColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => SizedBox(
return Container(
height: iconSize, height: iconSize,
width: iconSize, width: iconSize,
child: RawMaterialButton( child: RawMaterialButton(
elevation: 0, elevation: 0,
highlightElevation: 0, highlightElevation: 0,
focusElevation: 0, focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0, hoverElevation: 0,
onPressed: onPressed, onPressed: onPressed,
fillColor: fillColor:
@@ -99,16 +109,15 @@ class _IconButton extends StatelessWidget {
), ),
); );
} }
}
class _PreparingState extends StatelessWidget { class _PreparingState extends StatelessWidget {
final String attachmentId;
const _PreparingState({ const _PreparingState({
Key? key, Key? key,
required this.attachmentId, required this.attachmentId,
}) : super(key: key); }) : super(key: key);
final String attachmentId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -138,10 +147,6 @@ class _PreparingState extends StatelessWidget {
} }
class _InProgressState extends StatelessWidget { class _InProgressState extends StatelessWidget {
final int sent;
final int total;
final String attachmentId;
const _InProgressState({ const _InProgressState({
Key? key, Key? key,
required this.sent, required this.sent,
@@ -149,6 +154,10 @@ class _InProgressState extends StatelessWidget {
required this.attachmentId, required this.attachmentId,
}) : super(key: key); }) : super(key: key);
final int sent;
final int total;
final String attachmentId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -178,10 +187,6 @@ class _InProgressState extends StatelessWidget {
} }
class _FailedState extends StatelessWidget { class _FailedState extends StatelessWidget {
final String? error;
final String messageId;
final String attachmentId;
const _FailedState({ const _FailedState({
Key? key, Key? key,
this.error, this.error,
@@ -189,6 +194,10 @@ class _FailedState extends StatelessWidget {
required this.attachmentId, required this.attachmentId,
}) : super(key: key); }) : super(key: key);
final String? error;
final String messageId;
final String attachmentId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -232,16 +241,14 @@ class _FailedState extends StatelessWidget {
class _SuccessState extends StatelessWidget { class _SuccessState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Align(
return Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: CircleAvatar( child: CircleAvatar(
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
maxRadius: 12.0, maxRadius: 12,
child: StreamSvgIcon.check( child: StreamSvgIcon.check(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
), ),
); );
} }
}
@@ -1,13 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_theme.dart'; /// Enum for identifying type of attachment
enum AttachmentSource { enum AttachmentSource {
/// Attachment is attached
local, local,
/// Attachment is uploaded
network, network,
} }
/// Extension for identifying type of attachment
extension AttachmentSourceX on AttachmentSource { extension AttachmentSourceX on AttachmentSource {
/// The [when] method is the equivalent to pattern matching. /// The [when] method is the equivalent to pattern matching.
/// Its prototype depends on the AttachmentSource defined. /// Its prototype depends on the AttachmentSource defined.
@@ -25,18 +29,9 @@ extension AttachmentSourceX on AttachmentSource {
} }
} }
/// Abstract class for deriving attachment types
abstract class AttachmentWidget extends StatelessWidget { abstract class AttachmentWidget extends StatelessWidget {
final Size? size; /// Constructor for creating attachment widget
final AttachmentSource? _source;
final Message message;
final Attachment attachment;
AttachmentSource get source =>
_source ??
(attachment.file != null
? AttachmentSource.local
: AttachmentSource.network);
const AttachmentWidget({ const AttachmentWidget({
Key? key, Key? key,
required this.message, required this.message,
@@ -45,23 +40,43 @@ abstract class AttachmentWidget extends StatelessWidget {
AttachmentSource? source, AttachmentSource? source,
}) : _source = source, }) : _source = source,
super(key: key); super(key: key);
/// Size of attachments
final Size? size;
final AttachmentSource? _source;
/// Message which attachment is attached to
final Message message;
/// Attachment to display
final Attachment attachment;
/// Getter for source of attachment
AttachmentSource get source =>
_source ??
(attachment.file != null
? AttachmentSource.local
: AttachmentSource.network);
} }
/// Widget for building in case of error
class AttachmentError extends StatelessWidget { class AttachmentError extends StatelessWidget {
final Size? size; /// Constructor for creating AttachmentError
const AttachmentError({ const AttachmentError({
Key? key, Key? key,
this.size, this.size,
}) : super(key: key); }) : super(key: key);
/// Size of error
final Size? size;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Center(
return Center(
child: Container( child: Container(
width: size?.width, width: size?.width,
height: size?.height, height: size?.height,
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1), color:
StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
child: Center( child: Center(
child: Icon( child: Icon(
Icons.error_outline, Icons.error_outline,
@@ -71,4 +86,3 @@ class AttachmentError extends StatelessWidget {
), ),
); );
} }
}
@@ -6,15 +6,14 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import '../upload_progress_indicator.dart'; // ignore: always_use_package_imports
import 'attachment_widget.dart'; import 'attachment_widget.dart';
/// Widget for displaying file attachments
class FileAttachment extends AttachmentWidget { class FileAttachment extends AttachmentWidget {
final Widget? title; /// Constructor for creating a widget when attachment is of type 'file'
final Widget? trailing;
final VoidCallback? onAttachmentTap;
const FileAttachment({ const FileAttachment({
Key? key, Key? key,
required Message message, required Message message,
@@ -30,8 +29,19 @@ class FileAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// Title for attachment
final Widget? title;
/// Widget for displaying at the end of attachment (such as a download button)
final Widget? trailing;
/// Callback called when attachment widget is tapped
final VoidCallback? onAttachmentTap;
/// Check if attachment is a video
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video'; bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
/// Check if attachment is an image
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image'; bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
@override @override
@@ -42,7 +52,7 @@ class FileAttachment extends AttachmentWidget {
onTap: onAttachmentTap, onTap: onAttachmentTap,
child: Container( child: Container(
width: size?.width ?? 100, width: size?.width ?? 100,
height: 56.0, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorTheme.white, color: colorTheme.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -54,12 +64,12 @@ class FileAttachment extends AttachmentWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
height: 40.0, height: 40,
width: 33.33, width: 33.33,
margin: EdgeInsets.all(8.0), margin: const EdgeInsets.all(8),
child: _getFileTypeImage(context), child: _getFileTypeImage(context),
), ),
SizedBox(width: 8.0), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -71,12 +81,12 @@ class FileAttachment extends AttachmentWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 3.0), const SizedBox(height: 3),
_buildSubtitle(context), _buildSubtitle(context),
], ],
), ),
), ),
SizedBox(width: 8.0), const SizedBox(width: 8),
_buildTrailing(context), _buildTrailing(context),
], ],
), ),
@@ -85,12 +95,10 @@ class FileAttachment extends AttachmentWidget {
); );
} }
ShapeBorder _getDefaultShape(BuildContext context) { ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
return RoundedRectangleBorder( side: const BorderSide(width: 0, color: Colors.transparent),
side: BorderSide(width: 0.0, color: Colors.transparent),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
); );
}
Widget _getFileTypeImage(BuildContext context) { Widget _getFileTypeImage(BuildContext context) {
if (isImageAttachment) { if (isImageAttachment) {
@@ -106,10 +114,8 @@ class FileAttachment extends AttachmentWidget {
return Image.memory( return Image.memory(
attachment.file!.bytes!, attachment.file!.bytes!,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, obj, trace) { errorBuilder: (_, obj, trace) =>
return getFileTypeImage( getFileTypeImage(attachment.extraData['other'] as String?),
attachment.extraData['other'] as String?);
},
); );
}, },
network: () { network: () {
@@ -124,10 +130,8 @@ class FileAttachment extends AttachmentWidget {
attachment.assetUrl ?? attachment.assetUrl ??
attachment.thumbUrl!, attachment.thumbUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (_, obj, trace) { errorWidget: (_, obj, trace) =>
return getFileTypeImage( getFileTypeImage(attachment.extraData['other'] as String?),
attachment.extraData['other'] as String?);
},
placeholder: (_, __) { placeholder: (_, __) {
final image = Image.asset( final image = Image.asset(
'images/placeholder.png', 'images/placeholder.png',
@@ -156,27 +160,23 @@ class FileAttachment extends AttachmentWidget {
child: source.when( child: source.when(
local: () => VideoThumbnailImage( local: () => VideoThumbnailImage(
video: attachment.file!.path!, video: attachment.file!.path!,
placeholderBuilder: (_) { placeholderBuilder: (_) => const Center(
return Center( child: SizedBox(
child: Container( width: 20,
width: 20.0, height: 20,
height: 20.0, child: CircularProgressIndicator(),
child: const CircularProgressIndicator(), ),
), ),
);
},
), ),
network: () => VideoThumbnailImage( network: () => VideoThumbnailImage(
video: attachment.assetUrl!, video: attachment.assetUrl!,
placeholderBuilder: (_) { placeholderBuilder: (_) => const Center(
return Center( child: SizedBox(
child: Container( width: 20,
width: 20.0, height: 20,
height: 20.0, child: CircularProgressIndicator(),
child: const CircularProgressIndicator(), ),
), ),
);
},
), ),
), ),
); );
@@ -189,23 +189,22 @@ class FileAttachment extends AttachmentWidget {
double iconSize = 24.0, double iconSize = 24.0,
VoidCallback? onPressed, VoidCallback? onPressed,
Color? fillColor, Color? fillColor,
}) { }) =>
return Container( SizedBox(
height: iconSize, height: iconSize,
width: iconSize, width: iconSize,
child: RawMaterialButton( child: RawMaterialButton(
elevation: 0, elevation: 0,
highlightElevation: 0, highlightElevation: 0,
focusElevation: 0, focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0, hoverElevation: 0,
onPressed: onPressed, onPressed: onPressed,
fillColor: fillColor, fillColor: fillColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: icon, child: icon,
), ),
); );
}
Widget _buildTrailing(BuildContext context) { Widget _buildTrailing(BuildContext context) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -214,7 +213,7 @@ class FileAttachment extends AttachmentWidget {
var trailingWidget = trailing; var trailingWidget = trailing;
trailingWidget ??= attachment.uploadState.when( trailingWidget ??= attachment.uploadState.when(
preparing: () => Padding( preparing: () => Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: _buildButton( child: _buildButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.white), icon: StreamSvgIcon.close(color: theme.colorTheme.white),
fillColor: theme.colorTheme.overlayDark, fillColor: theme.colorTheme.overlayDark,
@@ -222,7 +221,7 @@ class FileAttachment extends AttachmentWidget {
), ),
), ),
inProgress: (_, __) => Padding( inProgress: (_, __) => Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: _buildButton( child: _buildButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.white), icon: StreamSvgIcon.close(color: theme.colorTheme.white),
fillColor: theme.colorTheme.overlayDark, fillColor: theme.colorTheme.overlayDark,
@@ -230,15 +229,15 @@ class FileAttachment extends AttachmentWidget {
), ),
), ),
success: () => Padding( success: () => Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: CircleAvatar( child: CircleAvatar(
backgroundColor: theme.colorTheme.accentBlue, backgroundColor: theme.colorTheme.accentBlue,
maxRadius: 12.0, maxRadius: 12,
child: StreamSvgIcon.check(color: theme.colorTheme.white), child: StreamSvgIcon.check(color: theme.colorTheme.white),
), ),
), ),
failed: (_) => Padding( failed: (_) => Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: _buildButton( child: _buildButton(
icon: StreamSvgIcon.retry(color: theme.colorTheme.white), icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
fillColor: theme.colorTheme.overlayDark, fillColor: theme.colorTheme.overlayDark,
@@ -251,7 +250,6 @@ class FileAttachment extends AttachmentWidget {
) ?? ) ??
IconButton( IconButton(
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black), icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
splashRadius: 16, splashRadius: 16,
onPressed: () { onPressed: () {
@@ -262,7 +260,6 @@ class FileAttachment extends AttachmentWidget {
if (message.status == MessageSendingStatus.sent) { if (message.status == MessageSendingStatus.sent) {
trailingWidget = IconButton( trailingWidget = IconButton(
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black), icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
splashRadius: 16, splashRadius: 16,
onPressed: () { onPressed: () {
@@ -284,29 +281,25 @@ class FileAttachment extends AttachmentWidget {
color: theme.colorTheme.grey, color: theme.colorTheme.grey,
); );
return attachment.uploadState.when( return attachment.uploadState.when(
preparing: () { preparing: () => UploadProgressIndicator(
return UploadProgressIndicator(
uploaded: 0, uploaded: 0,
total: double.maxFinite.toInt(), total: double.maxFinite.toInt(),
showBackground: false, showBackground: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
textStyle: textStyle, textStyle: textStyle,
progressIndicatorColor: theme.colorTheme.accentBlue, progressIndicatorColor: theme.colorTheme.accentBlue,
); ),
}, inProgress: (sent, total) => UploadProgressIndicator(
inProgress: (sent, total) {
return UploadProgressIndicator(
uploaded: sent, uploaded: sent,
total: total, total: total,
showBackground: false, showBackground: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
textStyle: textStyle, textStyle: textStyle,
progressIndicatorColor: theme.colorTheme.accentBlue, progressIndicatorColor: theme.colorTheme.accentBlue,
); ),
}, success: () => Text(fileSize(size), style: textStyle),
success: () => Text('${fileSize(size, 2)}', style: textStyle),
failed: (_) => Text('UPLOAD ERROR', style: textStyle), failed: (_) => Text('UPLOAD ERROR', style: textStyle),
) ?? ) ??
Text('${fileSize(size)}', style: textStyle); Text(fileSize(size), style: textStyle);
} }
} }
@@ -1,18 +1,13 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../full_screen_media.dart'; /// Widget for showing a GIF attachment
import '../stream_chat_theme.dart';
import '../stream_svg_icon.dart';
import 'attachment_widget.dart';
class GiphyAttachment extends AttachmentWidget { class GiphyAttachment extends AttachmentWidget {
final ShowMessageCallback? onShowMessage; /// Constructor for creating a [GiphyAttachment] widget
final ValueChanged<ReturnActionType>? onReturnAction;
final VoidCallback? onAttachmentTap;
const GiphyAttachment({ const GiphyAttachment({
Key? key, Key? key,
required Message message, required Message message,
@@ -28,12 +23,21 @@ class GiphyAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final imageUrl = final imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl; attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
if (imageUrl == null) { if (imageUrl == null) {
return AttachmentError(); return const AttachmentError();
} }
if (attachment.actions.isNotEmpty) { if (attachment.actions.isNotEmpty) {
return _buildSendingAttachment(context, imageUrl); return _buildSendingAttachment(context, imageUrl);
@@ -50,12 +54,11 @@ class GiphyAttachment extends AttachmentWidget {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
elevation: 2, elevation: 2,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
bottomRight: Radius.circular(0.0), topLeft: Radius.circular(16),
topLeft: Radius.circular(16.0), bottomLeft: Radius.circular(16),
bottomLeft: Radius.circular(16.0),
), ),
), ),
child: Column( child: Column(
@@ -63,16 +66,16 @@ class GiphyAttachment extends AttachmentWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.giphyIcon(), StreamSvgIcon.giphyIcon(),
SizedBox(width: 8), const SizedBox(width: 8),
Text( const Text(
'Giphy', 'Giphy',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
if (attachment.title != null) if (attachment.title != null)
Flexible( Flexible(
child: Text( child: Text(
@@ -91,25 +94,22 @@ class GiphyAttachment extends AttachmentWidget {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2),
child: GestureDetector( child: GestureDetector(
onTap: () => onAttachmentTap ?? _onImageTap(context), onTap: () => onAttachmentTap ?? _onImageTap(context),
child: CachedNetworkImage( child: CachedNetworkImage(
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
placeholder: (_, __) { placeholder: (_, __) => SizedBox(
return Container(
width: size?.width, width: size?.width,
height: size?.height, height: size?.height,
child: Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); ),
},
imageUrl: imageUrl, imageUrl: imageUrl,
errorWidget: (context, url, error) { errorWidget: (context, url, error) =>
return AttachmentError(size: size); AttachmentError(size: size),
},
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
@@ -123,11 +123,9 @@ class GiphyAttachment extends AttachmentWidget {
height: 0.5, height: 0.5,
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Container( child: SizedBox(
height: 50, height: 50,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
@@ -156,10 +154,10 @@ class GiphyAttachment extends AttachmentWidget {
.colorTheme .colorTheme
.black .black
.withOpacity(0.2), .withOpacity(0.2),
height: 50.0, height: 50,
), ),
Expanded( Expanded(
child: Container( child: SizedBox(
height: 50, height: 50,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
@@ -189,10 +187,10 @@ class GiphyAttachment extends AttachmentWidget {
.colorTheme .colorTheme
.black .black
.withOpacity(0.2), .withOpacity(0.2),
height: 50.0, height: 50,
), ),
Expanded( Expanded(
child: Container( child: SizedBox(
height: 50, height: 50,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
@@ -217,11 +215,11 @@ class GiphyAttachment extends AttachmentWidget {
], ],
), ),
), ),
SizedBox(height: 4.0), const SizedBox(height: 4),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -230,10 +228,10 @@ class GiphyAttachment extends AttachmentWidget {
.colorTheme .colorTheme
.black .black
.withOpacity(0.5), .withOpacity(0.5),
size: 16.0, size: 16,
), ),
SizedBox( const SizedBox(
width: 8.0, width: 8,
), ),
Text( Text(
'Only visible to you', 'Only visible to you',
@@ -275,8 +273,8 @@ class GiphyAttachment extends AttachmentWidget {
if (res != null) onReturnAction?.call(res); if (res != null) onReturnAction?.call(res);
} }
Widget _buildSentAttachment(BuildContext context, String imageUrl) { Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
return Container( SizedBox(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
final res = final res =
@@ -314,9 +312,8 @@ class GiphyAttachment extends AttachmentWidget {
); );
}, },
imageUrl: imageUrl, imageUrl: imageUrl,
errorWidget: (context, url, error) { errorWidget: (context, url, error) =>
return AttachmentError(size: size); AttachmentError(size: size),
},
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
Positioned( Positioned(
@@ -332,8 +329,8 @@ class GiphyAttachment extends AttachmentWidget {
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, horizontal: 8,
vertical: 4.0, vertical: 4,
), ),
child: Row( child: Row(
children: [ children: [
@@ -359,4 +356,3 @@ class GiphyAttachment extends AttachmentWidget {
), ),
); );
} }
}
@@ -1,21 +1,15 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../full_screen_media.dart'; /// Widget for showing an image attachment
import '../stream_chat_theme.dart';
import 'attachment_title.dart';
import 'attachment_widget.dart';
class ImageAttachment extends AttachmentWidget { class ImageAttachment extends AttachmentWidget {
final MessageTheme messageTheme; /// Constructor for creating a [ImageAttachment] widget
final bool showTitle;
final ShowMessageCallback? onShowMessage;
final ValueChanged<ReturnActionType>? onReturnAction;
final VoidCallback? onAttachmentTap;
const ImageAttachment({ const ImageAttachment({
Key? key, Key? key,
required Message message, required Message message,
@@ -33,9 +27,23 @@ class ImageAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// [MessageTheme] for showing image title
final MessageTheme messageTheme;
/// Flag for showing title
final bool showTitle;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => source.when(
return source.when(
local: () { local: () {
if (attachment.localUri == null || attachment.file?.bytes == null) { if (attachment.localUri == null || attachment.file?.bytes == null) {
return AttachmentError(size: size); return AttachmentError(size: size);
@@ -47,12 +55,10 @@ class ImageAttachment extends AttachmentWidget {
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, _, __) { errorBuilder: (context, _, __) => Image.asset(
return Image.asset(
'images/placeholder.png', 'images/placeholder.png',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
); ),
},
), ),
); );
}, },
@@ -103,18 +109,15 @@ class ImageAttachment extends AttachmentWidget {
); );
}, },
imageUrl: imageUrl, imageUrl: imageUrl,
errorWidget: (context, url, error) { errorWidget: (context, url, error) => AttachmentError(size: size),
return AttachmentError(size: size);
},
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
); );
}, },
); );
}
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) { Widget _buildImageAttachment(BuildContext context, Widget imageWidget) =>
return ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints.loose(size!), constraints: BoxConstraints.loose(size!),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
@@ -128,7 +131,8 @@ class ImageAttachment extends AttachmentWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) { builder: (_) {
final channel = StreamChannel.of(context).channel; final channel =
StreamChannel.of(context).channel;
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: FullScreenMedia( child: FullScreenMedia(
@@ -146,7 +150,7 @@ class ImageAttachment extends AttachmentWidget {
child: imageWidget, child: imageWidget,
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder( child: AttachmentUploadStateBuilder(
message: message, message: message,
attachment: attachment, attachment: attachment,
@@ -167,4 +171,3 @@ class ImageAttachment extends AttachmentWidget {
), ),
); );
} }
}
@@ -1,18 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'attachment_title.dart'; /// Widget for showing a video attachment
import 'attachment_upload_state_builder.dart';
import 'attachment_widget.dart';
class VideoAttachment extends AttachmentWidget { class VideoAttachment extends AttachmentWidget {
final MessageTheme messageTheme; /// Constructor for creating a [VideoAttachment] widget
final ShowMessageCallback? onShowMessage;
final ValueChanged<ReturnActionType>? onReturnAction;
final VoidCallback? onAttachmentTap;
const VideoAttachment({ const VideoAttachment({
Key? key, Key? key,
required Message message, required Message message,
@@ -29,9 +24,20 @@ class VideoAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// [MessageTheme] for showing title
final MessageTheme messageTheme;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => source.when(
return source.when(
local: () { local: () {
if (attachment.file == null) { if (attachment.file == null) {
return AttachmentError(size: size); return AttachmentError(size: size);
@@ -63,10 +69,9 @@ class VideoAttachment extends AttachmentWidget {
); );
}, },
); );
}
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) { Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) =>
return ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints.loose(size ?? Size.infinite), constraints: BoxConstraints.loose(size ?? Size.infinite),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
@@ -94,17 +99,17 @@ class VideoAttachment extends AttachmentWidget {
child: Stack( child: Stack(
children: [ children: [
videoWidget, videoWidget,
Center( const Center(
child: Material( child: Material(
shape: CircleBorder(), shape: CircleBorder(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16),
child: Icon(Icons.play_arrow), child: Icon(Icons.play_arrow),
), ),
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder( child: AttachmentUploadStateBuilder(
message: message, message: message,
attachment: attachment, attachment: attachment,
@@ -126,4 +131,3 @@ class VideoAttachment extends AttachmentWidget {
), ),
); );
} }
}
@@ -5,9 +5,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import '../stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'extension.dart';
/// Callback to download an attachment asset /// Callback to download an attachment asset
typedef AttachmentDownloader = Future<String> Function( typedef AttachmentDownloader = Future<String> Function(
@@ -17,6 +16,16 @@ typedef AttachmentDownloader = Future<String> Function(
/// Widget that shows the options in the gallery view /// Widget that shows the options in the gallery view
class AttachmentActionsModal extends StatelessWidget { class AttachmentActionsModal extends StatelessWidget {
/// Returns a new [AttachmentActionsModal]
const AttachmentActionsModal({
Key? key,
required this.currentIndex,
required this.message,
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
}) : super(key: key);
/// The message containing the attachments /// The message containing the attachments
final Message message; final Message message;
@@ -32,39 +41,28 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to provide download files /// Callback to provide download files
final AttachmentDownloader? fileDownloader; final AttachmentDownloader? fileDownloader;
/// Returns a new [AttachmentActionsModal]
const AttachmentActionsModal({
required this.currentIndex,
required this.message,
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context), onTap: () => Navigator.maybePop(context),
child: _buildPage(context), child: _buildPage(context),
); );
}
Widget _buildPage(context) { Widget _buildPage(context) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
SizedBox(height: kToolbarHeight), const SizedBox(height: kToolbarHeight),
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8),
child: Container( child: Container(
width: MediaQuery.of(context).size.width * 0.5, width: MediaQuery.of(context).size.width * 0.5,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16),
), ),
child: Container( child: SizedBox(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -73,7 +71,7 @@ class AttachmentActionsModal extends StatelessWidget {
context, context,
'Reply', 'Reply',
StreamSvgIcon.iconCurveLineLeftUp( StreamSvgIcon.iconCurveLineLeftUp(
size: 24.0, size: 24,
color: theme.colorTheme.grey, color: theme.colorTheme.grey,
), ),
() { () {
@@ -84,16 +82,17 @@ class AttachmentActionsModal extends StatelessWidget {
context, context,
'Show in Chat', 'Show in Chat',
StreamSvgIcon.eye( StreamSvgIcon.eye(
size: 24.0, size: 24,
color: theme.colorTheme.black, color: theme.colorTheme.black,
), ),
onShowMessage, onShowMessage,
), ),
_buildButton( _buildButton(
context, context,
// ignore: lines_longer_than_80_chars
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}', 'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.iconSave( StreamSvgIcon.iconSave(
size: 24.0, size: 24,
color: theme.colorTheme.grey, color: theme.colorTheme.grey,
), ),
() { () {
@@ -144,7 +143,7 @@ class AttachmentActionsModal extends StatelessWidget {
context, context,
'Delete', 'Delete',
StreamSvgIcon.delete( StreamSvgIcon.delete(
size: 24.0, size: 24,
color: theme.colorTheme.accentRed, color: theme.colorTheme.accentRed,
), ),
() { () {
@@ -194,18 +193,18 @@ class AttachmentActionsModal extends StatelessWidget {
VoidCallback? onTap, { VoidCallback? onTap, {
Color? color, Color? color,
Key? key, Key? key,
}) { }) =>
return Material( Material(
key: key, key: key,
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row( child: Row(
children: [ children: [
icon, icon,
SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
title, title,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -218,7 +217,6 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
), ),
); );
}
Widget _buildDownloadProgressDialog( Widget _buildDownloadProgressDialog(
BuildContext context, BuildContext context,
@@ -247,7 +245,7 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
child: Center( child: Center(
child: progress == null child: progress == null
? Container( ? SizedBox(
height: 100, height: 100,
width: 100, width: 100,
child: StreamSvgIcon.error( child: StreamSvgIcon.error(
@@ -255,15 +253,15 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
) )
: progress.toProgressIndicatorValue == 1.0 : progress.toProgressIndicatorValue == 1.0
? Container( ? SizedBox(
key: Key('completedIcon'), key: const Key('completedIcon'),
height: 160, height: 160,
width: 160, width: 160,
child: StreamSvgIcon.check( child: StreamSvgIcon.check(
color: theme.colorTheme.greyGainsboro, color: theme.colorTheme.greyGainsboro,
), ),
) )
: Container( : SizedBox(
height: 100, height: 100,
width: 100, width: 100,
child: Stack( child: Stack(
@@ -271,7 +269,7 @@ class AttachmentActionsModal extends StatelessWidget {
children: [ children: [
CircularProgressIndicator( CircularProgressIndicator(
value: progress.toProgressIndicatorValue, value: progress.toProgressIndicatorValue,
strokeWidth: 8.0, strokeWidth: 8,
valueColor: AlwaysStoppedAnimation( valueColor: AlwaysStoppedAnimation(
theme.colorTheme.accentBlue, theme.colorTheme.accentBlue,
), ),
@@ -317,14 +315,13 @@ class AttachmentActionsModal extends StatelessWidget {
} }
class _DownloadProgress { class _DownloadProgress {
final int total;
final int received;
const _DownloadProgress(this.total, this.received); const _DownloadProgress(this.total, this.received);
factory _DownloadProgress.initial() { factory _DownloadProgress.initial() =>
return _DownloadProgress(double.maxFinite.toInt(), 0); _DownloadProgress(double.maxFinite.toInt(), 0);
}
final int total;
final int received;
double get toProgressIndicatorValue => received / total; double get toProgressIndicatorValue => received / total;
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/src/unread_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart'; /// Back button implementation
class StreamBackButton extends StatelessWidget { class StreamBackButton extends StatelessWidget {
/// Constructor for creating back button
const StreamBackButton({ const StreamBackButton({
Key? key, Key? key,
this.onPressed, this.onPressed,
@@ -12,15 +13,17 @@ class StreamBackButton extends StatelessWidget {
this.cid, this.cid,
}) : super(key: key); }) : super(key: key);
/// Callback for when button is pressed
final VoidCallback? onPressed; final VoidCallback? onPressed;
/// Show unread count
final bool showUnreads; final bool showUnreads;
/// Channel cid used to retrieve unread count /// Channel cid used to retrieve unread count
final String? cid; final String? cid;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Stack(
return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
RawMaterialButton( RawMaterialButton(
@@ -30,7 +33,6 @@ class StreamBackButton extends StatelessWidget {
elevation: 0, elevation: 0,
highlightElevation: 0, highlightElevation: 0,
focusElevation: 0, focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0, hoverElevation: 0,
onPressed: () { onPressed: () {
if (onPressed != null) { if (onPressed != null) {
@@ -39,7 +41,7 @@ class StreamBackButton extends StatelessWidget {
Navigator.maybePop(context); Navigator.maybePop(context);
} }
}, },
padding: const EdgeInsets.all(14.0), padding: const EdgeInsets.all(14),
child: StreamSvgIcon.left( child: StreamSvgIcon.left(
size: 24, size: 24,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.black,
@@ -56,4 +58,3 @@ class StreamBackButton extends StatelessWidget {
], ],
); );
} }
}
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart'; /// Bottom Sheet with options
import 'channel_info.dart';
import 'option_list_tile.dart';
class ChannelBottomSheet extends StatefulWidget { class ChannelBottomSheet extends StatefulWidget {
final VoidCallback? onViewInfoTap; /// Constructor for creating bottom sheet
const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key);
ChannelBottomSheet({this.onViewInfoTap}); /// Callback when 'View Info' is tapped
final VoidCallback? onViewInfoTap;
@override @override
_ChannelBottomSheetState createState() => _ChannelBottomSheetState(); _ChannelBottomSheetState createState() => _ChannelBottomSheetState();
@@ -29,31 +30,31 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
return Material( return Material(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16),
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
), ),
), ),
child: !_showActions child: !_showActions
? SizedBox() ? const SizedBox()
: ListView( : ListView(
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
SizedBox( const SizedBox(
height: 24.0, height: 24,
), ),
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChannelName( child: ChannelName(
textStyle: textStyle:
StreamChatTheme.of(context).textTheme.headlineBold, StreamChatTheme.of(context).textTheme.headlineBold,
), ),
), ),
), ),
SizedBox( const SizedBox(
height: 5.0, height: 5,
), ),
Center( Center(
child: ChannelInfo( child: ChannelInfo(
@@ -64,8 +65,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
.subtitle, .subtitle,
), ),
), ),
SizedBox( const SizedBox(
height: 17.0, height: 17,
), ),
if (channel.isDistinct && channel.memberCount == 2) if (channel.isDistinct && channel.memberCount == 2)
Column( Column(
@@ -75,16 +76,16 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
.firstWhere( .firstWhere(
(e) => e.user?.id != userAsMember.user?.id) (e) => e.user?.id != userAsMember.user?.id)
.user!, .user!,
constraints: BoxConstraints( constraints: const BoxConstraints(
maxHeight: 64.0, maxHeight: 64,
maxWidth: 64.0, maxWidth: 64,
), ),
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32),
onlineIndicatorConstraints: onlineIndicatorConstraints:
BoxConstraints.tight(Size(12.0, 12.0)), BoxConstraints.tight(const Size(12, 12)),
), ),
SizedBox( const SizedBox(
height: 6.0, height: 6,
), ),
Text( Text(
members members
@@ -102,29 +103,28 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
), ),
if (!(channel.isDistinct && channel.memberCount == 2)) if (!(channel.isDistinct && channel.memberCount == 2))
Container( Container(
height: 94.0, height: 94,
alignment: Alignment.center, alignment: Alignment.center,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: members.length, itemCount: members.length,
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, index) { itemBuilder: (context, index) => Padding(
return Padding( padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column( child: Column(
children: [ children: [
UserAvatar( UserAvatar(
user: members[index].user!, user: members[index].user!,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 64.0, height: 64,
width: 64.0, width: 64,
), ),
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32),
onlineIndicatorConstraints: onlineIndicatorConstraints:
BoxConstraints.tight(Size(12.0, 12.0)), BoxConstraints.tight(const Size(12, 12)),
), ),
SizedBox( const SizedBox(
height: 6.0, height: 6,
), ),
Text( Text(
members[index].user?.name ?? '', members[index].user?.name ?? '',
@@ -136,16 +136,15 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
), ),
], ],
), ),
);
},
), ),
), ),
SizedBox( ),
height: 24.0, const SizedBox(
height: 24,
), ),
OptionListTile( OptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.user( child: StreamSvgIcon.user(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.grey,
), ),
@@ -156,7 +155,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
if (!channel.isDistinct) if (!channel.isDistinct)
OptionListTile( OptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.userRemove( child: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.grey,
), ),
@@ -175,7 +174,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
if (isOwner) if (isOwner)
OptionListTile( OptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.delete( child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
@@ -195,7 +194,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
), ),
OptionListTile( OptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.closeSmall( child: StreamSvgIcon.closeSmall(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.grey,
), ),
@@ -4,13 +4,9 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/channel_name.dart';
import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import './channel_name.dart';
import '../stream_chat_flutter.dart';
import 'channel_image.dart';
import 'connection_status_builder.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
/// ///
@@ -43,16 +39,36 @@ import 'connection_status_builder.dart';
/// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget. /// However you can also use it as a normal widget.
/// ///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel. /// Make sure to have a [StreamChannel] ancestor in order to provide the
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates. /// information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel
/// information as soon as it updates.
/// ///
/// By default the widget shows a backButton that calls [Navigator.pop]. /// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just override the behaviour /// You can disable this button using the [showBackButton] property of just
/// override the behaviour
/// with [onBackPressed]. /// with [onBackPressed].
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
const ChannelHeader({
Key? key,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.showTypingIndicator = true,
this.onImageTap,
this.showConnectionStateTile = false,
this.title,
this.subtitle,
this.leading,
this.actions,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// True if this header shows the leading back button /// True if this header shows the leading back button
final bool showBackButton; final bool showBackButton;
@@ -69,6 +85,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// If true the typing indicator will be rendered if a user is typing /// If true the typing indicator will be rendered if a user is typing
final bool showTypingIndicator; final bool showTypingIndicator;
/// Show connection tile on header
final bool showConnectionStateTile; final bool showConnectionStateTile;
/// Title widget /// Title widget
@@ -84,22 +101,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it shows the [ChannelImage] /// By default it shows the [ChannelImage]
final List<Widget>? actions; final List<Widget>? actions;
/// Creates a channel header
ChannelHeader({
Key? key,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.showTypingIndicator = true,
this.onImageTap,
this.showConnectionStateTile = false,
this.title,
this.subtitle,
this.leading,
this.actions,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -110,7 +111,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
onPressed: onBackPressed, onPressed: onBackPressed,
showUnreads: true, showUnreads: true,
) )
: SizedBox()); : const SizedBox());
return ConnectionStatusBuilder( return ConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
@@ -131,6 +132,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
} }
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions
showMessage: showConnectionStateTile ? showStatus : false, showMessage: showConnectionStateTile ? showStatus : false,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
@@ -144,7 +146,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
actions: actions ?? actions: actions ??
<Widget>[ <Widget>[
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0), padding: const EdgeInsets.only(right: 10),
child: Center( child: Center(
child: ChannelImage( child: ChannelImage(
borderRadius: StreamChatTheme.of(context) borderRadius: StreamChatTheme.of(context)
@@ -165,11 +167,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
centerTitle: true, centerTitle: true,
title: InkWell( title: InkWell(
onTap: onTitleTap, onTap: onTitleTap,
child: Container( child: SizedBox(
height: preferredSize.height, height: preferredSize.height,
width: preferredSize.width, width: preferredSize.width,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
title ?? title ??
@@ -179,7 +180,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
.channelHeaderTheme .channelHeaderTheme
.title, .title,
), ),
SizedBox(height: 2), const SizedBox(height: 2),
subtitle ?? subtitle ??
ChannelInfo( ChannelInfo(
showTypingIndicator: showTypingIndicator, showTypingIndicator: showTypingIndicator,
@@ -36,12 +36,14 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// } /// }
/// ``` /// ```
/// ///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. /// 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. /// By default the widget radius size is 40x40 pixels.
/// Set the property [constraints] to set a custom dimension. /// Set the property [constraints] to set a custom dimension.
/// ///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelImage extends StatelessWidget { class ChannelImage extends StatelessWidget {
/// Instantiate a new ChannelImage /// Instantiate a new ChannelImage
@@ -56,6 +58,7 @@ class ChannelImage extends StatelessWidget {
this.selectionThickness = 4, this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
/// [BorderRadius] to display the widget
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// The channel to show the image of /// The channel to show the image of
@@ -67,10 +70,13 @@ class ChannelImage extends StatelessWidget {
/// The function called when the image is tapped /// The function called when the image is tapped
final VoidCallback? onTap; final VoidCallback? onTap;
/// If image is selected
final bool selected; final bool selected;
/// Selection color for image
final Color? selectionColor; final Color? selectionColor;
/// Thickness of selection image
final double selectionThickness; final double selectionThickness;
@override @override
@@ -91,8 +97,7 @@ class ChannelImage extends StatelessWidget {
stream: streamChat.client.state.usersStream.map( stream: streamChat.client.state.usersStream.map(
(users) => users[otherMember?.userId] ?? otherMember!.user!), (users) => users[otherMember?.userId] ?? otherMember!.user!),
initialData: otherMember!.user, initialData: otherMember!.user,
builder: (context, snapshot) { builder: (context, snapshot) => UserAvatar(
return UserAvatar(
borderRadius: borderRadius ?? borderRadius: borderRadius ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
@@ -109,14 +114,14 @@ class ChannelImage extends StatelessWidget {
selectionColor: selectionColor ?? selectionColor: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue, StreamChatTheme.of(context).colorTheme.accentBlue,
selectionThickness: selectionThickness, selectionThickness: selectionThickness,
); ));
});
} else { } else {
final images = channel.state?.members final images = channel.state?.members
.where((member) => .where((member) =>
member.user?.id != streamChat.user?.id && member.user?.id != streamChat.user?.id &&
member.user?.extraData['image'] != null) member.user?.extraData['image'] != null)
.take(4) .take(4)
// ignore: cast_nullable_to_non_nullable
.map((e) => e.user?.extraData['image'] as String) .map((e) => e.user?.extraData['image'] as String)
.toList(); .toList();
return GroupImage( return GroupImage(
@@ -158,27 +163,24 @@ class ChannelImage extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
image != null if (image != null)
? CachedNetworkImage( CachedNetworkImage(
imageUrl: image, imageUrl: image,
errorWidget: (_, __, ___) { errorWidget: (_, __, ___) => Center(
return Center(
child: Text( child: Text(
snapshot.data?.containsKey('name') ?? false snapshot.data?.containsKey('name') ?? false
? snapshot.data!['name'][0] ? snapshot.data!['name'][0]
: '', : '',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context).colorTheme.white,
.colorTheme
.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
); ),
},
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: StreamChatTheme.of(context).defaultChannelImage( else
StreamChatTheme.of(context).defaultChannelImage(
context, context,
channel, channel,
), ),
@@ -194,7 +196,7 @@ class ChannelImage extends StatelessWidget {
); );
if (selected) { if (selected) {
child = ClipRRect( child = ClipRRect(
key: Key('selectedImage'), key: const Key('selectedImage'),
borderRadius: (borderRadius ?? borderRadius: (borderRadius ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.ownMessageTheme .ownMessageTheme
@@ -3,9 +3,17 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'connection_status_builder.dart'; /// Widget which shows channel info
class ChannelInfo extends StatelessWidget { class ChannelInfo extends StatelessWidget {
/// Constructor which creates a [ChannelInfo] widget
const ChannelInfo({
Key? key,
required this.channel,
this.textStyle,
this.showTypingIndicator = true,
}) : super(key: key);
/// The channel about which the info is to be displayed
final Channel channel; final Channel channel;
/// The style of the text displayed /// The style of the text displayed
@@ -14,21 +22,13 @@ class ChannelInfo extends StatelessWidget {
/// If true the typing indicator will be rendered if a user is typing /// If true the typing indicator will be rendered if a user is typing
final bool showTypingIndicator; final bool showTypingIndicator;
const ChannelInfo({
Key? key,
required this.channel,
this.textStyle,
this.showTypingIndicator = true,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
return StreamBuilder<List<Member>>( return StreamBuilder<List<Member>>(
stream: channel.state?.membersStream, stream: channel.state?.membersStream,
initialData: channel.state?.members, initialData: channel.state?.members,
builder: (context, snapshot) { builder: (context, snapshot) => ConnectionStatusBuilder(
return ConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
@@ -38,17 +38,16 @@ class ChannelInfo extends StatelessWidget {
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
return _buildDisconnectedTitleState(context, client); return _buildDisconnectedTitleState(context, client);
default: default:
return Offstage(); return const Offstage();
} }
}, },
); ),
},
); );
} }
Widget _buildConnectedTitleState( Widget _buildConnectedTitleState(
BuildContext context, List<Member>? members) { BuildContext context, List<Member>? members) {
var alternativeWidget; Widget? alternativeWidget;
if (channel.memberCount != null && channel.memberCount! > 2) { if (channel.memberCount != null && channel.memberCount! > 2) {
var text = '${channel.memberCount} Members'; var text = '${channel.memberCount} Members';
@@ -82,7 +81,7 @@ class ChannelInfo extends StatelessWidget {
} }
if (!showTypingIndicator) { if (!showTypingIndicator) {
return alternativeWidget ?? Offstage(); return alternativeWidget ?? const Offstage();
} }
return TypingIndicator( return TypingIndicator(
@@ -92,31 +91,29 @@ class ChannelInfo extends StatelessWidget {
); );
} }
Widget _buildConnectingTitleState(BuildContext context) { Widget _buildConnectingTitleState(BuildContext context) => Row(
return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( const SizedBox(
height: 16, height: 16,
width: 16, width: 16,
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'Searching for Network', 'Searching for Network',
style: textStyle, style: textStyle,
), ),
], ],
); );
}
Widget _buildDisconnectedTitleState( Widget _buildDisconnectedTitleState(
BuildContext context, BuildContext context,
StreamChatClient client, StreamChatClient client,
) { ) =>
return Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
@@ -127,7 +124,7 @@ class ChannelInfo extends StatelessWidget {
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity( visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity,
), ),
@@ -146,4 +143,3 @@ class ChannelInfo extends StatelessWidget {
], ],
); );
} }
}
@@ -6,10 +6,7 @@ import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'connection_status_builder.dart'; /// Widget builder for title
import 'info_tile.dart';
import 'stream_chat.dart';
typedef TitleBuilder = Widget Function( typedef TitleBuilder = Widget Function(
BuildContext context, BuildContext context,
ConnectionStatus status, ConnectionStatus status,
@@ -42,10 +39,13 @@ typedef TitleBuilder = Widget Function(
/// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget. /// However you can also use it as a normal widget.
/// ///
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status. /// The widget by default uses the inherited [StreamChatClient]
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree. /// to fetch information about the status.
/// However you can also pass your own [StreamChatClient]
/// if you don't have it in the widget tree.
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelListHeaderTheme] property. /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelListHeaderTheme] property.
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader /// Instantiates a ChannelListHeader
@@ -75,8 +75,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// Callback to call when pressing the new chat button. /// Callback to call when pressing the new chat button.
final VoidCallback? onNewChatButtonTap; final VoidCallback? onNewChatButtonTap;
/// Show connection state tile
final bool showConnectionStateTile; final bool showConnectionStateTile;
/// Callback before navigation is performed
final VoidCallback? preNavigationCallback; final VoidCallback? preNavigationCallback;
/// Subtitle widget /// Subtitle widget
@@ -113,6 +115,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
} }
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions
showMessage: showConnectionStateTile ? showStatus : false, showMessage: showConnectionStateTile ? showStatus : false,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
@@ -143,7 +146,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
.avatarTheme .avatarTheme
?.constraints, ?.constraints,
) )
: Offstage(), : const Offstage(),
), ),
actions: actions ?? actions: actions ??
[ [
@@ -151,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
child: IconButton( child: IconButton(
icon: ConnectionStatusBuilder( icon: ConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
var color; Color? color;
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
color = StreamChatTheme.of(context) color = StreamChatTheme.of(context)
@@ -168,8 +171,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
return SvgPicture.asset( return SvgPicture.asset(
'svgs/icon_pen_write.svg', 'svgs/icon_pen_write.svg',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
width: 24.0, width: 24,
height: 24.0, height: 24,
color: color, color: color,
); );
}, },
@@ -193,11 +196,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
return _buildDisconnectedTitleState(context, _client); return _buildDisconnectedTitleState(context, _client);
default: default:
return Offstage(); return const Offstage();
} }
}, },
), ),
subtitle ?? Offstage(), subtitle ?? const Offstage(),
], ],
), ),
), ),
@@ -213,18 +216,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
); );
Widget _buildConnectingTitleState(BuildContext context) { Widget _buildConnectingTitleState(BuildContext context) => Row(
return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( const SizedBox(
height: 16, height: 16,
width: 16, width: 16,
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'Searching for Network', 'Searching for Network',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -237,13 +239,12 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
], ],
); );
}
Widget _buildDisconnectedTitleState( Widget _buildDisconnectedTitleState(
BuildContext context, BuildContext context,
StreamChatClient client, StreamChatClient client,
) { ) =>
return Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
@@ -275,8 +276,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
], ],
); );
}
@override @override
Size get preferredSize => Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
} }
@@ -3,20 +3,19 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
import 'channel_bottom_sheet.dart';
import 'channel_preview.dart';
/// Callback called when tapping on a channel /// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget?); typedef ChannelTapCallback = void Function(Channel, Widget?);
/// Builder used to create a custom [ChannelPreview] from a [Channel] /// Builder used to create a custom [ChannelPreview] from a [Channel]
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
/// Callback for when 'View Info' is tapped
typedef ViewInfoCallback = void Function(Channel); typedef ViewInfoCallback = void Function(Channel);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
@@ -47,14 +46,16 @@ typedef ViewInfoCallback = void Function(Channel);
/// ``` /// ```
/// ///
/// ///
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. /// Make sure to have a [StreamChat] ancestor in order to provide the
/// information about the channels.
/// The widget uses a [ListView.custom] to render the list of channels. /// The widget uses a [ListView.custom] to render the list of channels.
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// The widget components render the ui based on the first ancestor of
/// type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelListView extends StatefulWidget { class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView /// Instantiate a new ChannelListView
ChannelListView({ const ChannelListView({
Key? key, Key? key,
this.filter, this.filter,
this.options, this.options,
@@ -94,8 +95,10 @@ class ChannelListView extends StatefulWidget {
final Map<String, dynamic>? options; final Map<String, dynamic>? options;
/// The sorting used for the channels matching the filters. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// 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. /// Direction can be ascending or descending.
final List<SortOption<ChannelModel>>? sort; final List<SortOption<ChannelModel>>? sort;
@@ -137,8 +140,10 @@ class ChannelListView extends StatefulWidget {
/// The amount of space by which to inset the children. /// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
/// List of selected channels which are displayed differently
final List<Channel> selectedChannels; final List<Channel> selectedChannels;
/// Callback for when 'View Info' is tapped
final ViewInfoCallback? onViewInfoTap; final ViewInfoCallback? onViewInfoTap;
/// The builder that will be used in case of error /// The builder that will be used in case of error
@@ -203,15 +208,14 @@ class _ChannelListViewState extends State<ChannelListView> {
crossAxisCount: widget.crossAxisCount, crossAxisCount: widget.crossAxisCount,
), ),
itemCount: channels.length, itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) =>
return _gridItemBuilder(context, index, channels); _gridItemBuilder(context, index, channels),
},
); );
} else { } else {
child = ListView.separated( child = ListView.separated(
padding: widget.padding, padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length, channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) { separatorBuilder: (_, index) {
@@ -220,9 +224,8 @@ class _ChannelListViewState extends State<ChannelListView> {
} }
return _separatorBuilder(context, index); return _separatorBuilder(context, index);
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) =>
return _listItemBuilder(context, index, channels); _listItemBuilder(context, index, channels),
},
); );
} }
} }
@@ -233,11 +236,9 @@ class _ChannelListViewState extends State<ChannelListView> {
); );
} }
Widget _buildEmptyWidget(BuildContext context) { Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
return LayoutBuilder( builder: (context, viewportConstraints) => SingleChildScrollView(
builder: (context, viewportConstraints) { physics: const AlwaysScrollableScrollPhysics(),
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Stack( child: Stack(
children: [ children: [
ConstrainedBox( ConstrainedBox(
@@ -248,7 +249,7 @@ class _ChannelListViewState extends State<ChannelListView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: StreamSvgIcon.message( child: StreamSvgIcon.message(
size: 136, size: 136,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
@@ -257,7 +258,7 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Text( child: Text(
'Lets start chatting!', 'Lets start chatting!',
style: StreamChatTheme.of(context).textTheme.headline, style: StreamChatTheme.of(context).textTheme.headline,
@@ -265,7 +266,7 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: 52, horizontal: 52,
), ),
child: Text( child: Text(
@@ -307,15 +308,12 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
], ],
), ),
),
); );
},
);
}
Widget _buildLoadingWidget(BuildContext context) { Widget _buildLoadingWidget(BuildContext context) => ListView(
return ListView(
padding: widget.padding, padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: List.generate( children: List.generate(
25, 25,
(i) { (i) {
@@ -331,7 +329,6 @@ class _ChannelListViewState extends State<ChannelListView> {
}, },
), ),
); );
}
Shimmer _buildLoadingItem(BuildContext context) { Shimmer _buildLoadingItem(BuildContext context) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
@@ -340,25 +337,25 @@ class _ChannelListViewState extends State<ChannelListView> {
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke, highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column( child: Column(
children: [ children: [
SizedBox(height: 4.0), const SizedBox(height: 4),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
for (int i = 0; i < widget.crossAxisCount; i++) for (int i = 0; i < widget.crossAxisCount; i++)
Container( Container(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 70, height: 70,
width: 70, width: 70,
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 16.0, height: 16,
), ),
], ],
), ),
@@ -373,7 +370,7 @@ class _ChannelListViewState extends State<ChannelListView> {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
@@ -389,7 +386,7 @@ class _ChannelListViewState extends State<ChannelListView> {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 16, height: 16,
width: 82, width: 82,
), ),
@@ -406,7 +403,7 @@ class _ChannelListViewState extends State<ChannelListView> {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.expand( constraints: const BoxConstraints.expand(
height: 16, height: 16,
), ),
), ),
@@ -418,7 +415,7 @@ class _ChannelListViewState extends State<ChannelListView> {
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 16, height: 16,
width: 42, width: 42,
), ),
@@ -430,18 +427,17 @@ class _ChannelListViewState extends State<ChannelListView> {
} }
} }
Widget _buildErrorWidget(BuildContext context, Object error) { Widget _buildErrorWidget(BuildContext context, Object error) => Center(
return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text.rich( Text.rich(
TextSpan( const TextSpan(
children: [ children: [
WidgetSpan( WidgetSpan(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: EdgeInsets.only(
right: 2.0, right: 2,
), ),
child: Icon(Icons.error_outline), child: Icon(Icons.error_outline),
), ),
@@ -453,12 +449,11 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
TextButton( TextButton(
onPressed: () => _channelListController.loadData!(), onPressed: () => _channelListController.loadData!(),
child: Text('Retry'), child: const Text('Retry'),
), ),
], ],
), ),
); );
}
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) { Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
final channelsProvider = ChannelsBloc.of(context); final channelsProvider = ChannelsBloc.of(context);
@@ -471,13 +466,11 @@ class _ChannelListViewState extends State<ChannelListView> {
key: ValueKey<String>('CHANNEL-${channel.id}'), key: ValueKey<String>('CHANNEL-${channel.id}'),
channel: channel, channel: channel,
child: Builder( child: Builder(
builder: (context) { builder: (context) => Slidable(
return Slidable(
controller: _slideController, controller: _slideController,
enabled: widget.swipeToAction, enabled: widget.swipeToAction,
actionPane: SlidableBehindActionPane(), actionPane: const SlidableBehindActionPane(),
actionExtentRatio: 0.12, actionExtentRatio: 0.12,
closeOnScroll: true,
secondaryActions: <Widget>[ secondaryActions: <Widget>[
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
@@ -485,23 +478,21 @@ class _ChannelListViewState extends State<ChannelListView> {
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(32), topLeft: Radius.circular(32),
topRight: Radius.circular(32), topRight: Radius.circular(32),
), ),
), ),
context: context, context: context,
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: channel, channel: channel,
child: ChannelBottomSheet( child: ChannelBottomSheet(
onViewInfoTap: () { onViewInfoTap: () {
widget.onViewInfoTap?.call(channel); widget.onViewInfoTap?.call(channel);
}, },
), ),
); ),
},
); );
}, },
), ),
@@ -526,8 +517,7 @@ class _ChannelListViewState extends State<ChannelListView> {
'Are you sure you want to delete this conversation?', 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: color: StreamChatTheme.of(context).colorTheme.accentRed,
StreamChatTheme.of(context).colorTheme.accentRed,
), ),
); );
if (res == true) { if (res == true) {
@@ -546,8 +536,7 @@ class _ChannelListViewState extends State<ChannelListView> {
onTap: (channel) => onTap(channel, widget.channelWidget), onTap: (channel) => onTap(channel, widget.channelWidget),
), ),
), ),
); ),
},
), ),
); );
} else { } else {
@@ -567,12 +556,10 @@ class _ChannelListViewState extends State<ChannelListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: client, channel: client,
child: widget.channelWidget!, child: widget.channelWidget!,
); ),
},
), ),
); );
}; };
@@ -589,24 +576,23 @@ class _ChannelListViewState extends State<ChannelListView> {
key: ValueKey<String>('CHANNEL-${channel.id}'), key: ValueKey<String>('CHANNEL-${channel.id}'),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
ChannelImage( ChannelImage(
channel: channel, channel: channel,
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
selected: selected, selected: selected,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
width: 64, width: 64,
height: 64, height: 64,
), ),
onTap: () => _getChannelTap(context), onTap: () => _getChannelTap(context),
), ),
SizedBox(height: 7), const SizedBox(height: 7),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: ChannelName( child: const ChannelName(
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -622,8 +608,8 @@ class _ChannelListViewState extends State<ChannelListView> {
Widget _buildQueryProgressIndicator( Widget _buildQueryProgressIndicator(
context, context,
ChannelsBlocState channelsProvider, ChannelsBlocState channelsProvider,
) { ) =>
return StreamBuilder<bool>( StreamBuilder<bool>(
stream: channelsProvider.queryChannelsLoading, stream: channelsProvider.queryChannelsLoading,
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -633,8 +619,8 @@ class _ChannelListViewState extends State<ChannelListView> {
.colorTheme .colorTheme
.accentRed .accentRed
.withOpacity(.2), .withOpacity(.2),
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: Text('Error loading channels'), child: Text('Error loading channels'),
), ),
@@ -642,18 +628,17 @@ class _ChannelListViewState extends State<ChannelListView> {
); );
} }
return snapshot.data! return snapshot.data!
? Center( ? const Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16),
child: const CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: Offstage(); : const Offstage();
}); });
}
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) {
var effect = StreamChatTheme.of(context).colorTheme.borderBottom; final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
return Container( return Container(
height: 1, height: 1,
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
/// It shows the current [Channel] name using a [Text] widget. /// 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. /// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
class ChannelName extends StatelessWidget { class ChannelName extends StatelessWidget {
/// Instantiate a new ChannelName /// Instantiate a new ChannelName
const ChannelName({ const ChannelName({
@@ -25,9 +25,8 @@ class ChannelName extends StatelessWidget {
return StreamBuilder<Map<String, dynamic>>( return StreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream, stream: channel.extraDataStream,
initialData: channel.extraData, initialData: channel.extraData,
builder: (context, snapshot) { builder: (context, snapshot) =>
return _buildName(snapshot.data!, channel.state?.members, client); _buildName(snapshot.data!, channel.state?.members, client),
},
); );
} }
@@ -35,8 +34,8 @@ class ChannelName extends StatelessWidget {
Map<String, dynamic> extraData, Map<String, dynamic> extraData,
List<Member>? members, List<Member>? members,
StreamChatState client, StreamChatState client,
) { ) =>
return LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
String? title; String? title;
if (extraData['name'] == null) { if (extraData['name'] == null) {
@@ -50,7 +49,8 @@ class ChannelName extends StatelessWidget {
var currentChars = 0; var currentChars = 0;
final currentMembers = <Member>[]; final currentMembers = <Member>[];
otherMembers!.forEach((element) { otherMembers!.forEach((element) {
final newLength = currentChars + (element.user?.name.length ?? 0); final newLength =
currentChars + (element.user?.name.length ?? 0);
if (newLength < maxChars) { if (newLength < maxChars) {
currentChars = newLength; currentChars = newLength;
currentMembers.add(element); currentMembers.add(element);
@@ -59,8 +59,8 @@ class ChannelName extends StatelessWidget {
final exceedingMembers = final exceedingMembers =
otherMembers.length - currentMembers.length; otherMembers.length - currentMembers.length;
title = title = '${currentMembers.map((e) => e.user?.name).join(', ')} '
'${currentMembers.map((e) => e.user?.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else { } else {
title = 'No title'; title = 'No title';
} }
@@ -76,4 +76,3 @@ class ChannelName extends StatelessWidget {
}, },
); );
} }
}
@@ -3,23 +3,38 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
/// ///
/// It shows the current [Channel] preview. /// It shows the current [Channel] preview.
/// ///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. /// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
/// ///
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView]. /// Usually you don't use this widget as it's the default channel preview
/// used by [ChannelListView].
/// ///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelPreview extends StatelessWidget { class ChannelPreview extends StatelessWidget {
/// Constructor for creating [ChannelPreview]
const ChannelPreview({
required this.channel,
Key? key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
}) : super(key: key);
/// Function called when tapping this widget /// Function called when tapping this widget
final void Function(Channel)? onTap; final void Function(Channel)? onTap;
@@ -38,36 +53,25 @@ class ChannelPreview extends StatelessWidget {
/// Widget rendering the subtitle /// Widget rendering the subtitle
final Widget? subtitle; final Widget? subtitle;
/// Widget rendering the leading element, by default it shows the [ChannelImage] /// Widget rendering the leading element, by default
/// it shows the [ChannelImage]
final Widget? leading; final Widget? leading;
/// Widget rendering the trailing element, by default it shows the last message date /// Widget rendering the trailing element,
/// by default it shows the last message date
final Widget? trailing; final Widget? trailing;
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget /// Widget rendering the sending indicator,
/// by default it uses the [SendingIndicator] widget
final Widget? sendingIndicator; final Widget? sendingIndicator;
ChannelPreview({
required this.channel,
Key? key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
return StreamBuilder<bool>( return StreamBuilder<bool>(
stream: channel.isMutedStream, stream: channel.isMutedStream,
initialData: channel.isMuted, initialData: channel.isMuted,
builder: (context, snapshot) { builder: (context, snapshot) => Opacity(
return Opacity(
opacity: snapshot.data! ? 0.5 : 1, opacity: snapshot.data! ? 0.5 : 1,
child: ListTile( child: ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -105,7 +109,7 @@ class ChannelPreview extends StatelessWidget {
snapshot.data!.isEmpty || snapshot.data!.isEmpty ||
!snapshot.data!.any((Member e) => !snapshot.data!.any((Member e) =>
e.user!.id == channel.client.state.user?.id)) { e.user!.id == channel.client.state.user?.id)) {
return SizedBox(); return const SizedBox();
} }
return UnreadIndicator( return UnreadIndicator(
cid: channel.cid, cid: channel.cid,
@@ -128,7 +132,7 @@ class ChannelPreview extends StatelessWidget {
if (lastMessage?.user?.id == if (lastMessage?.user?.id ==
StreamChat.of(context).user?.id) { StreamChat.of(context).user?.id) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4),
child: SendingIndicator( child: SendingIndicator(
message: lastMessage!, message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize, size: channelPreviewTheme.indicatorIconSize,
@@ -143,37 +147,37 @@ class ChannelPreview extends StatelessWidget {
), ),
); );
} }
return SizedBox(); return const SizedBox();
}, },
), ),
trailing ?? _buildDate(context), trailing ?? _buildDate(context),
], ],
), ),
), ),
); ));
});
} }
Widget _buildDate(BuildContext context) { Widget _buildDate(BuildContext context) => StreamBuilder<DateTime?>(
return StreamBuilder<DateTime?>(
stream: channel.lastMessageAtStream, stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt, initialData: channel.lastMessageAt,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return SizedBox(); return const SizedBox();
} }
final lastMessageAt = snapshot.data!.toLocal(); final lastMessageAt = snapshot.data!.toLocal();
String stringDate; String stringDate;
final now = DateTime.now(); final now = DateTime.now();
var startOfDay = DateTime(now.year, now.month, now.day); final startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >= if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) { startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).jm; stringDate = Jiffy(lastMessageAt.toLocal()).jm;
} else if (lastMessageAt.millisecondsSinceEpoch >= } else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { startOfDay
.subtract(const Duration(days: 1))
.millisecondsSinceEpoch) {
stringDate = 'Yesterday'; stringDate = 'Yesterday';
} else if (startOfDay.difference(lastMessageAt).inDays < 7) { } else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
@@ -183,11 +187,11 @@ class ChannelPreview extends StatelessWidget {
return Text( return Text(
stringDate, stringDate,
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, style:
StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
); );
}, },
); );
}
Widget _buildSubtitle(BuildContext context) { Widget _buildSubtitle(BuildContext context) {
if (channel.isMuted) { if (channel.isMuted) {
@@ -211,15 +215,15 @@ class ChannelPreview extends StatelessWidget {
); );
} }
Widget _buildLastMessage(BuildContext context) { Widget _buildLastMessage(BuildContext context) =>
return StreamBuilder<List<Message>?>( StreamBuilder<List<Message>?>(
stream: channel.state!.messagesStream, stream: channel.state!.messagesStream,
initialData: channel.state!.messages, initialData: channel.state!.messages,
builder: (context, snapshot) { builder: (context, snapshot) {
final lastMessage = snapshot.data final lastMessage = snapshot.data
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
if (lastMessage == null) { if (lastMessage == null) {
return SizedBox(); return const SizedBox();
} }
var text = lastMessage.text; var text = lastMessage.text;
@@ -246,7 +250,10 @@ class ChannelPreview extends StatelessWidget {
text, text,
lastMessage.mentionedUsers, lastMessage.mentionedUsers,
lastMessage.attachments, lastMessage.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith( StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
?.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
.subtitle .subtitle
@@ -254,7 +261,10 @@ class ChannelPreview extends StatelessWidget {
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic ? FontStyle.italic
: FontStyle.normal), : FontStyle.normal),
StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith( StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
?.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
.subtitle .subtitle
@@ -270,7 +280,6 @@ class ChannelPreview extends StatelessWidget {
); );
}, },
); );
}
TextSpan _getDisplayText( TextSpan _getDisplayText(
String text, String text,
@@ -298,7 +307,7 @@ class ChannelPreview extends StatelessWidget {
)); ));
} else { } else {
resList.add(TextSpan( resList.add(TextSpan(
text: e == textList.last ? '$e' : '$e ', text: e == textList.last ? e : '$e ',
style: normalTextStyle, style: normalTextStyle,
)); ));
} }
@@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'stream_chat.dart';
/// Widget that builds itself based on the latest snapshot of interaction with /// Widget that builds itself based on the latest snapshot of interaction with
/// a [Stream] of type [ConnectionStatus]. /// a [Stream] of type [ConnectionStatus].
/// ///
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] in case no /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
/// stream is provided. /// in case no stream is provided.
class ConnectionStatusBuilder extends StatelessWidget { class ConnectionStatusBuilder extends StatelessWidget {
/// Creates a new ConnectionStatusBuilder /// Creates a new ConnectionStatusBuilder
const ConnectionStatusBuilder({ const ConnectionStatusBuilder({
@@ -47,11 +46,11 @@ class ConnectionStatusBuilder extends StatelessWidget {
if (errorBuilder != null) { if (errorBuilder != null) {
return errorBuilder!(context, snapshot.error); return errorBuilder!(context, snapshot.error);
} }
return Offstage(); return const Offstage();
} }
if (!snapshot.hasData) { if (!snapshot.hasData) {
if (loadingBuilder != null) return loadingBuilder!(context); if (loadingBuilder != null) return loadingBuilder!(context);
return Offstage(); return const Offstage();
} }
return statusBuilder(context, snapshot.data!); return statusBuilder(context, snapshot.data!);
}, },
@@ -4,15 +4,19 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// It shows a date divider depending on the date difference /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { class DateDivider extends StatelessWidget {
final DateTime dateTime; /// Constructor for creating a [DateDivider]
final bool uppercase;
const DateDivider({ const DateDivider({
Key? key, Key? key,
required this.dateTime, required this.dateTime,
this.uppercase = false, this.uppercase = false,
}) : super(key: key); }) : super(key: key);
/// [DateTime] to display
final DateTime dateTime;
/// If text is uppercase
final bool uppercase;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final createdAt = Jiffy(dateTime); final createdAt = Jiffy(dateTime);
@@ -22,10 +26,10 @@ class DateDivider extends StatelessWidget {
if (Jiffy(createdAt).isSame(now, Units.DAY)) { if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'Today'; dayInfo = 'Today';
} else if (Jiffy(createdAt) } else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) { .isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
dayInfo = 'Yesterday'; dayInfo = 'Yesterday';
} else if (Jiffy(createdAt).isAfter( } else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)), now.subtract(const Duration(days: 7)),
Units.DAY, Units.DAY,
)) { )) {
dayInfo = createdAt.EEEE; dayInfo = createdAt.EEEE;
@@ -3,7 +3,9 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// Widget to display deleted message
class DeletedMessage extends StatelessWidget { class DeletedMessage extends StatelessWidget {
/// Constructor to create [DeletedMessage]
const DeletedMessage({ const DeletedMessage({
Key? key, Key? key,
required this.messageTheme, required this.messageTheme,
@@ -29,8 +31,7 @@ class DeletedMessage extends StatelessWidget {
final bool reverse; final bool reverse;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Transform(
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0), transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Material( child: Material(
@@ -53,7 +54,7 @@ class DeletedMessage extends StatelessWidget {
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: 16, horizontal: 16,
), ),
child: Transform( child: Transform(
@@ -71,4 +72,3 @@ class DeletedMessage extends StatelessWidget {
), ),
); );
} }
}
@@ -9,9 +9,7 @@ final _emojis = Emoji.all();
/// String extension /// String extension
extension StringExtension on String { extension StringExtension on String {
/// Returns the capitalized string /// Returns the capitalized string
String capitalize() { String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
return '${this[0].toUpperCase()}${substring(1)}';
}
/// Returns whether the string contains only emoji's or not. /// Returns whether the string contains only emoji's or not.
/// ///
@@ -46,7 +44,9 @@ extension PlatformFileX on PlatformFile {
); );
} }
///
extension InputDecorationX on InputDecoration { extension InputDecorationX on InputDecoration {
///
InputDecoration merge(InputDecoration? other) { InputDecoration merge(InputDecoration? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -98,7 +98,9 @@ extension InputDecorationX on InputDecoration {
} }
} }
/// Gets text scale factor through context
extension BuildContextX on BuildContext { extension BuildContextX on BuildContext {
// ignore: public_member_api_docs
double get textScaleFactor => double get textScaleFactor =>
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0;
} }
@@ -8,24 +8,23 @@ import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart'; import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import '../stream_chat_flutter.dart'; /// Return action for coming back from pages
enum ReturnActionType {
/// No return action
none,
enum ReturnActionType { none, reply } /// Go to reply message action
reply,
}
/// Callback when show message is tapped
typedef ShowMessageCallback = void Function(Message message, Channel channel); typedef ShowMessageCallback = void Function(Message message, Channel channel);
/// A full screen image widget /// A full screen image widget
class FullScreenMedia extends StatefulWidget { class FullScreenMedia extends StatefulWidget {
/// The url of the image
final List<Attachment> mediaAttachments;
final Message message;
final int startIndex;
final String userName;
final ShowMessageCallback? onShowMessage;
/// Instantiate a new FullScreenImage /// Instantiate a new FullScreenImage
const FullScreenMedia({ const FullScreenMedia({
Key? key, Key? key,
@@ -37,6 +36,21 @@ class FullScreenMedia extends StatefulWidget {
}) : userName = userName ?? '', }) : userName = userName ?? '',
super(key: key); super(key: key);
/// The url of the image
final List<Attachment> mediaAttachments;
/// Message where attachments are attached
final Message message;
/// First index of media shown
final int startIndex;
/// Username of sender
final String userName;
/// Callback for when show message is tapped
final ShowMessageCallback? onShowMessage;
@override @override
_FullScreenMediaState createState() => _FullScreenMediaState(); _FullScreenMediaState createState() => _FullScreenMediaState();
} }
@@ -57,7 +71,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
super.initState(); super.initState();
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
); );
_pageController = PageController(initialPage: widget.startIndex); _pageController = PageController(initialPage: widget.startIndex);
_currentPage = widget.startIndex; _currentPage = widget.startIndex;
@@ -77,15 +91,13 @@ class _FullScreenMediaState extends State<FullScreenMedia>
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Scaffold(
return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
children: [ children: [
AnimatedBuilder( AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, snapshot) { builder: (context, snapshot) => PageView.builder(
return PageView.builder(
controller: _pageController, controller: _pageController,
onPageChanged: (val) { onPageChanged: (val) {
setState(() { setState(() {
@@ -133,7 +145,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
} else if (attachment.type == 'video') { } else if (attachment.type == 'video') {
final controller = videoPackages[attachment.id]!; final controller = videoPackages[attachment.id]!;
if (!controller.initialized) { if (!controller.initialized) {
return Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
@@ -150,7 +162,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 50.0, vertical: 50,
), ),
child: Chewie( child: Chewie(
controller: controller.chewieController!, controller: controller.chewieController!,
@@ -161,17 +173,17 @@ class _FullScreenMediaState extends State<FullScreenMedia>
return Container(); return Container();
}, },
itemCount: widget.mediaAttachments.length, itemCount: widget.mediaAttachments.length,
); )),
}),
AnimatedOpacity( AnimatedOpacity(
opacity: _optionsShown ? 1.0 : 0.0, opacity: _optionsShown ? 1.0 : 0.0,
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ImageHeader( ImageHeader(
userName: widget.userName, userName: widget.userName,
sentAt: sentAt:
// ignore: lines_longer_than_80_chars
'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}', 'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}',
onBackPressed: () { onBackPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -196,7 +208,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
_currentPage = val; _currentPage = val;
_pageController.animateToPage( _pageController.animateToPage(
val, val,
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
Navigator.pop(context); Navigator.pop(context);
@@ -209,7 +221,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
], ],
), ),
); );
}
String getDay(DateTime dateTime) { String getDay(DateTime dateTime) {
final now = DateTime.now(); final now = DateTime.now();
@@ -236,18 +247,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
} }
} }
/// Class for packaging up things required for videos
class VideoPackage { class VideoPackage {
final bool _showControls; /// Constructor for creating [VideoPackage]
final bool _autoInitialize;
final VideoPlayerController _videoPlayerController;
ChewieController? _chewieController;
VideoPlayerController get videoPlayer => _videoPlayerController;
ChewieController? get chewieController => _chewieController;
bool get initialized => _videoPlayerController.value.isInitialized;
VideoPackage( VideoPackage(
Attachment attachment, { Attachment attachment, {
bool showControls = false, bool showControls = false,
@@ -258,8 +260,22 @@ class VideoPackage {
? VideoPlayerController.file(File.fromUri(attachment.localUri!)) ? VideoPlayerController.file(File.fromUri(attachment.localUri!))
: VideoPlayerController.network(attachment.assetUrl!); : VideoPlayerController.network(attachment.assetUrl!);
Future<void> initialize() { final bool _showControls;
return _videoPlayerController.initialize().then((_) { final bool _autoInitialize;
final VideoPlayerController _videoPlayerController;
ChewieController? _chewieController;
/// Get video player for video
VideoPlayerController get videoPlayer => _videoPlayerController;
/// Get [ChewieController] for video
ChewieController? get chewieController => _chewieController;
/// Check if controller is initialised
bool get initialized => _videoPlayerController.value.isInitialized;
/// Initialize all things required for [VideoPackage]
Future<void> initialize() => _videoPlayerController.initialize().then((_) {
_chewieController = ChewieController( _chewieController = ChewieController(
videoPlayerController: _videoPlayerController, videoPlayerController: _videoPlayerController,
autoInitialize: _autoInitialize, autoInitialize: _autoInitialize,
@@ -267,16 +283,16 @@ class VideoPackage {
aspectRatio: _videoPlayerController.value.aspectRatio, aspectRatio: _videoPlayerController.value.aspectRatio,
); );
}); });
}
void addListener(VoidCallback listener) { /// Add a listener to video player controller
return _videoPlayerController.addListener(listener); void addListener(VoidCallback listener) =>
} _videoPlayerController.addListener(listener);
void removeListener(VoidCallback listener) { /// Remove a listener to video player controller
return _videoPlayerController.removeListener(listener); void removeListener(VoidCallback listener) =>
} _videoPlayerController.removeListener(listener);
/// Dispose controllers
Future<void> dispose() { Future<void> dispose() {
_chewieController?.dispose(); _chewieController?.dispose();
return _videoPlayerController.dispose(); return _videoPlayerController.dispose();
@@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart'; /// Widget for constructing a group of images
class GroupImage extends StatelessWidget { class GroupImage extends StatelessWidget {
/// Constructor for creating a [GroupImage]
const GroupImage({ const GroupImage({
Key? key, Key? key,
required this.images, required this.images,
@@ -15,17 +16,30 @@ class GroupImage extends StatelessWidget {
this.selectionThickness = 4, this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
/// List of images to display
final List<String> images; final List<String> images;
/// Constraints on the widget
final BoxConstraints? constraints; final BoxConstraints? constraints;
/// Callback when widget is tapped
final VoidCallback? onTap; final VoidCallback? onTap;
/// Highlights if selected
final bool selected; final bool selected;
/// [BorderRadius] to pass to the widget
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// Color of selection if selected
final Color? selectionColor; final Color? selectionColor;
/// Thickness with which color of selection is shown
final double selectionThickness; final double selectionThickness;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var avatar; Widget? avatar;
final streamChatTheme = StreamChatTheme.of(context); final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector( avatar = GestureDetector(
@@ -113,8 +127,8 @@ class GroupImage extends StatelessWidget {
child: Container( child: Container(
color: selectionColor ?? color: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue, StreamChatTheme.of(context).colorTheme.accentBlue,
height: 64.0, height: 64,
width: 64.0, width: 64,
child: Padding( child: Padding(
padding: EdgeInsets.all(selectionThickness), padding: EdgeInsets.all(selectionThickness),
child: avatar, child: avatar,
@@ -12,7 +12,22 @@ import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Footer widget for media display
class ImageFooter extends StatefulWidget implements PreferredSizeWidget { class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageFooter({
Key? key,
required this.message,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments = const [],
this.mediaSelectedCallBack,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// Callback to call when pressing the back button. /// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
final VoidCallback? onBackPressed; final VoidCallback? onBackPressed;
@@ -23,28 +38,21 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
/// Callback to call when the image is tapped. /// Callback to call when the image is tapped.
final VoidCallback? onImageTap; final VoidCallback? onImageTap;
/// Stores the current index of media shown
final int currentPage; final int currentPage;
/// Total number of pages of media
final int totalPages; final int totalPages;
/// All attachments to show
final List<Attachment> mediaAttachments; final List<Attachment> mediaAttachments;
/// Message which attachments are attached to
final Message message; final Message message;
/// Callback when media is selected
final ValueChanged<int>? mediaSelectedCallBack; final ValueChanged<int>? mediaSelectedCallBack;
/// Creates a channel header
ImageFooter({
Key? key,
required this.message,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments = const [],
this.mediaSelectedCallBack,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override @override
_ImageFooterState createState() => _ImageFooterState(); _ImageFooterState createState() => _ImageFooterState();
@@ -82,13 +90,14 @@ class _ImageFooterState extends State<ImageFooter> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
!showShareButton if (!showShareButton)
? Container( Container(
width: 48, width: 48,
) )
: IconButton( else
IconButton(
icon: StreamSvgIcon.iconShare( icon: StreamSvgIcon.iconShare(
size: 24.0, size: 24,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () async { onPressed: () async {
@@ -100,14 +109,12 @@ class _ImageFooterState extends State<ImageFooter> {
final type = attachment.type == 'image' final type = attachment.type == 'image'
? 'jpg' ? 'jpg'
: url.split('?').first.split('.').last; : url.split('?').first.split('.').last;
final request = final request = await HttpClient().getUrl(Uri.parse(url));
await HttpClient().getUrl(Uri.parse(url));
final response = await request.close(); final response = await request.close();
final bytes = final bytes =
await consolidateHttpClientResponseBytes(response); await consolidateHttpClientResponseBytes(response);
final tmpPath = await getTemporaryDirectory(); final tmpPath = await getTemporaryDirectory();
final filePath = final filePath = '${tmpPath.path}/${attachment.id}.$type';
'${tmpPath.path}/${attachment.id}.$type';
final file = File(filePath); final file = File(filePath);
await file.writeAsBytes(bytes); await file.writeAsBytes(bytes);
await Share.shareFiles( await Share.shareFiles(
@@ -120,9 +127,8 @@ class _ImageFooterState extends State<ImageFooter> {
), ),
InkWell( InkWell(
onTap: widget.onTitleTap, onTap: widget.onTitleTap,
child: Container( child: SizedBox(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@@ -156,12 +162,12 @@ class _ImageFooterState extends State<ImageFooter> {
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16),
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
), ),
), ),
builder: (context) { builder: (context) {
final crossAxisCount = 3; const crossAxisCount = 3;
final noOfRowToShowInitially = final noOfRowToShowInitially =
widget.mediaAttachments.length > crossAxisCount ? 2 : 1; widget.mediaAttachments.length > crossAxisCount ? 2 : 1;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -171,8 +177,7 @@ class _ImageFooterState extends State<ImageFooter> {
expand: false, expand: false,
initialChildSize: initialChildSize / size.height, initialChildSize: initialChildSize / size.height,
minChildSize: initialChildSize / size.height, minChildSize: initialChildSize / size.height,
builder: (context, scrollController) { builder: (context, scrollController) => SingleChildScrollView(
return SingleChildScrollView(
controller: scrollController, controller: scrollController,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -181,7 +186,7 @@ class _ImageFooterState extends State<ImageFooter> {
children: [ children: [
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16),
child: Text( child: Text(
'Photos', 'Photos',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -207,10 +212,12 @@ class _ImageFooterState extends State<ImageFooter> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: widget.mediaAttachments.length, itemCount: widget.mediaAttachments.length,
padding: const EdgeInsets.all(1), padding: const EdgeInsets.all(1),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // ignore: lines_longer_than_80_chars
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
mainAxisSpacing: 2.0, mainAxisSpacing: 2,
crossAxisSpacing: 2.0, crossAxisSpacing: 2,
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
Widget media; Widget media;
@@ -230,7 +237,7 @@ class _ImageFooterState extends State<ImageFooter> {
media = InkWell( media = InkWell(
onTap: () => widget.mediaSelectedCallBack!(index), onTap: () => widget.mediaSelectedCallBack!(index),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.0, aspectRatio: 1,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: attachment.imageUrl ?? imageUrl: attachment.imageUrl ??
attachment.assetUrl ?? attachment.assetUrl ??
@@ -246,7 +253,7 @@ class _ImageFooterState extends State<ImageFooter> {
media, media,
if (widget.message.user != null) if (widget.message.user != null)
Padding( Padding(
padding: EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Container( child: Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -254,7 +261,7 @@ class _ImageFooterState extends State<ImageFooter> {
color: Colors.white.withOpacity(0.6), color: Colors.white.withOpacity(0.6),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
blurRadius: 8.0, blurRadius: 8,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .black
@@ -266,7 +273,7 @@ class _ImageFooterState extends State<ImageFooter> {
child: UserAvatar( child: UserAvatar(
user: widget.message.user!, user: widget.message.user!,
constraints: constraints:
BoxConstraints.tight(Size(24, 24)), BoxConstraints.tight(const Size(24, 24)),
showOnlineStatus: false, showOnlineStatus: false,
), ),
), ),
@@ -278,8 +285,7 @@ class _ImageFooterState extends State<ImageFooter> {
), ),
], ],
), ),
); ),
},
); );
}, },
); );
@@ -287,7 +293,7 @@ class _ImageFooterState extends State<ImageFooter> {
/// Sends the current message /// Sends the current message
Future sendMessage() async { Future sendMessage() async {
var text = _messageController.text.trim(); final text = _messageController.text.trim();
final attachments = widget.message.attachments; final attachments = widget.message.attachments;
@@ -306,29 +312,3 @@ class _ImageFooterState extends State<ImageFooter> {
Navigator.pop(context); Navigator.pop(context);
} }
} }
/// Used for clipping textfield prefix icon
class IconClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var leftX = size.width / 5;
var rightX = 4 * size.width / 5;
var topY = size.height / 5;
var bottomY = 4 * size.height / 5;
final path = Path();
path.moveTo(leftX, topY);
path.lineTo(leftX, bottomY);
path.lineTo(rightX, bottomY);
path.lineTo(rightX, topY);
path.lineTo(leftX, topY);
path.lineTo(0.0, 0.0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
@@ -3,7 +3,9 @@ import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget for constructing a group of images in message
class ImageGroup extends StatelessWidget { class ImageGroup extends StatelessWidget {
/// Constructor for creating [ImageGroup] widget
const ImageGroup({ const ImageGroup({
Key? key, Key? key,
required this.images, required this.images,
@@ -13,36 +15,41 @@ class ImageGroup extends StatelessWidget {
this.onShowMessage, this.onShowMessage,
}) : super(key: key); }) : super(key: key);
/// List of attachments to show
final List<Attachment> images; final List<Attachment> images;
/// Message which images are attached to
final Message message; final Message message;
/// [MessageTheme] to apply to message
final MessageTheme messageTheme; final MessageTheme messageTheme;
/// Size of iamges
final Size size; final Size size;
/// Callback for when show message is tapped
final ShowMessageCallback? onShowMessage; final ShowMessageCallback? onShowMessage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => ConstrainedBox(
return ConstrainedBox(
constraints: BoxConstraints.loose(size), constraints: BoxConstraints.loose(size),
child: Flex( child: Flex(
direction: Axis.vertical, direction: Axis.vertical,
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
flex: 1,
fit: FlexFit.tight, fit: FlexFit.tight,
child: Flex( child: Flex(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
direction: Axis.horizontal, direction: Axis.horizontal,
children: [ children: [
Flexible( Flexible(
flex: 1,
fit: FlexFit.tight, fit: FlexFit.tight,
child: _buildImage(context, 0), child: _buildImage(context, 0),
), ),
Flexible( Flexible(
flex: 1,
fit: FlexFit.tight, fit: FlexFit.tight,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 2.0), padding: const EdgeInsets.only(left: 2),
child: _buildImage(context, 1), child: _buildImage(context, 1),
), ),
), ),
@@ -52,24 +59,21 @@ class ImageGroup extends StatelessWidget {
if (images.length >= 3) if (images.length >= 3)
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 1,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 2.0), padding: const EdgeInsets.only(top: 2),
child: Flex( child: Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 1,
child: _buildImage(context, 2), child: _buildImage(context, 2),
), ),
if (images.length >= 4) if (images.length >= 4)
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 1,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 2.0), padding: const EdgeInsets.only(left: 2),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
@@ -83,7 +87,7 @@ class ImageGroup extends StatelessWidget {
child: Center( child: Center(
child: Text( child: Text(
'+ ${images.length - 4}', '+ ${images.length - 4}',
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 26, fontSize: 26,
), ),
@@ -103,7 +107,6 @@ class ImageGroup extends StatelessWidget {
], ],
), ),
); );
}
void _onTap( void _onTap(
BuildContext context, BuildContext context,
@@ -128,8 +131,7 @@ class ImageGroup extends StatelessWidget {
); );
} }
Widget _buildImage(BuildContext context, int index) { Widget _buildImage(BuildContext context, int index) => ImageAttachment(
return ImageAttachment(
attachment: images[index], attachment: images[index],
size: size, size: size,
message: message, message: message,
@@ -137,4 +139,3 @@ class ImageGroup extends StatelessWidget {
onAttachmentTap: () => _onTap(context, index), onAttachmentTap: () => _onTap(context, index),
); );
} }
}
@@ -1,11 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment_actions_modal.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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/stream_svg_icon.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'attachment_actions_modal.dart'; /// Header/AppBar widget for media display screen
class ImageHeader extends StatelessWidget implements PreferredSizeWidget { class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageHeader({
Key? key,
required this.message,
this.currentIndex = 0,
this.showBackButton = true,
this.onBackPressed,
this.onShowMessage,
this.onTitleTap,
this.onImageTap,
this.userName = '',
this.sentAt = '',
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// True if this header shows the leading back button /// True if this header shows the leading back button
final bool showBackButton; final bool showBackButton;
@@ -22,42 +37,31 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// Callback to call when the image is tapped. /// Callback to call when the image is tapped.
final VoidCallback? onImageTap; final VoidCallback? onImageTap;
/// Message which attachments are attached to
final Message message; final Message message;
/// Username of sender
final String userName; final String userName;
/// Text which connotes the time the message was sent
final String sentAt; final String sentAt;
/// Stores the current index of media shown
final int currentIndex; final int currentIndex;
/// Creates a channel header
ImageHeader({
Key? key,
required this.message,
this.currentIndex = 0,
this.showBackButton = true,
this.onBackPressed,
this.onShowMessage,
this.onTitleTap,
this.onImageTap,
this.userName = '',
this.sentAt = '',
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => AppBar(
return AppBar(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
leading: showBackButton leading: showBackButton
? IconButton( ? IconButton(
icon: StreamSvgIcon.close( icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0, size: 24,
), ),
onPressed: onBackPressed, onPressed: onBackPressed,
) )
: SizedBox(), : const SizedBox(),
backgroundColor: backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[ actions: <Widget>[
@@ -75,17 +79,17 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
title: message.type != 'ephemeral' title: message.type != 'ephemeral'
? InkWell( ? InkWell(
onTap: onTitleTap, onTap: onTitleTap,
child: Container( child: SizedBox(
height: preferredSize.height, height: preferredSize.height,
width: preferredSize.width, width: preferredSize.width,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text( Text(
userName, userName,
style: StreamChatTheme.of(context).textTheme.headlineBold, style:
StreamChatTheme.of(context).textTheme.headlineBold,
), ),
Text( Text(
sentAt, sentAt,
@@ -97,9 +101,8 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
) )
: SizedBox(), : const SizedBox(),
); );
}
@override @override
final Size preferredSize; final Size preferredSize;
@@ -110,16 +113,14 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
final result = await showDialog( final result = await showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: channel, channel: channel,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: currentIndex, currentIndex: currentIndex,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
), ),
); ),
},
); );
if (result != null) { if (result != null) {
@@ -2,16 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// Tile to display a message, used in stream chat to display connection status
class InfoTile extends StatelessWidget { class InfoTile extends StatelessWidget {
final String message; /// Constructor for creating an [InfoTile] widget
final Widget child; const InfoTile({
final bool showMessage; Key? key,
final Alignment? tileAnchor;
final Alignment? childAnchor;
final TextStyle? textStyle;
final Color? backgroundColor;
InfoTile({
required this.message, required this.message,
required this.child, required this.child,
required this.showMessage, required this.showMessage,
@@ -19,16 +14,36 @@ class InfoTile extends StatelessWidget {
this.childAnchor, this.childAnchor,
this.textStyle, this.textStyle,
this.backgroundColor, this.backgroundColor,
}); }) : super(key: key);
/// String to display
final String message;
/// Widget to display over
final Widget child;
/// Flag to show message
final bool showMessage;
/// Anchor for tile - [portalAnchor] for [PortalEntry]
final Alignment? tileAnchor;
/// Alignment for child - [childAnchor] for [PortalEntry]
final Alignment? childAnchor;
/// [TextStyle] for message
final TextStyle? textStyle;
/// Background color for tile
final Color? backgroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => PortalEntry(
return PortalEntry(
visible: showMessage, visible: showMessage,
portalAnchor: tileAnchor ?? Alignment.topCenter, portalAnchor: tileAnchor ?? Alignment.topCenter,
childAnchor: childAnchor ?? Alignment.bottomCenter, childAnchor: childAnchor ?? Alignment.bottomCenter,
portal: Container( portal: Container(
height: 25.0, height: 25,
color: backgroundColor ?? color: backgroundColor ??
StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9), StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9),
child: Center( child: Center(
@@ -46,4 +61,3 @@ class InfoTile extends StatelessWidget {
child: child, child: child,
); );
} }
}
@@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart';
extension on Duration { extension on Duration {
String format() { String format() {
@@ -19,16 +18,21 @@ extension on Duration {
} }
} }
/// Constructs a list of media
class MediaListView extends StatefulWidget { class MediaListView extends StatefulWidget {
final List<String> selectedIds; /// Constructor for creating a [MediaListView] widget
final void Function(AssetEntity media)? onSelect;
const MediaListView({ const MediaListView({
Key? key, Key? key,
this.selectedIds = const [], this.selectedIds = const [],
this.onSelect, this.onSelect,
}) : super(key: key); }) : super(key: key);
/// Stores the media selected
final List<String> selectedIds;
/// Callback for on media selected
final void Function(AssetEntity media)? onSelect;
@override @override
_MediaListViewState createState() => _MediaListViewState(); _MediaListViewState createState() => _MediaListViewState();
} }
@@ -39,13 +43,12 @@ class _MediaListViewState extends State<MediaListView> {
int _currentPage = 0; int _currentPage = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => LazyLoadScrollView(
return LazyLoadScrollView(
onEndOfPage: () async => _getMedia(), onEndOfPage: () async => _getMedia(),
child: GridView.builder( child: GridView.builder(
itemCount: _media.length, itemCount: _media.length,
controller: _scrollController, controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
), ),
itemBuilder: ( itemBuilder: (
@@ -54,7 +57,7 @@ class _MediaListViewState extends State<MediaListView> {
) { ) {
final media = _media.elementAt(position); final media = _media.elementAt(position);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0), padding: const EdgeInsets.symmetric(horizontal: 1, vertical: 1),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (widget.onSelect != null) { if (widget.onSelect != null) {
@@ -64,10 +67,10 @@ class _MediaListViewState extends State<MediaListView> {
child: Stack( child: Stack(
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: 1.0, aspectRatio: 1,
child: FadeInImage( child: FadeInImage(
fadeInDuration: Duration(milliseconds: 300), fadeInDuration: const Duration(milliseconds: 300),
placeholder: AssetImage( placeholder: const AssetImage(
'images/placeholder.png', 'images/placeholder.png',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
), ),
@@ -80,8 +83,9 @@ class _MediaListViewState extends State<MediaListView> {
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
child: AnimatedOpacity( child: AnimatedOpacity(
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
opacity: widget.selectedIds.any((id) => id == media.id) opacity:
widget.selectedIds.any((id) => id == media.id)
? 1.0 ? 1.0
: 0.0, : 0.0,
child: Container( child: Container(
@@ -100,8 +104,9 @@ class _MediaListViewState extends State<MediaListView> {
StreamChatTheme.of(context).colorTheme.white, StreamChatTheme.of(context).colorTheme.white,
child: StreamSvgIcon.check( child: StreamSvgIcon.check(
size: 24, size: 24,
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black, .colorTheme
.black,
), ),
), ),
), ),
@@ -135,7 +140,6 @@ class _MediaListViewState extends State<MediaListView> {
}, },
), ),
); );
}
@override @override
void initState() { void initState() {
@@ -144,9 +148,7 @@ class _MediaListViewState extends State<MediaListView> {
} }
void _getMedia() async { void _getMedia() async {
final assetList = await PhotoManager.getAssetPathList( final assetList = await PhotoManager.getAssetPathList().then((value) {
hasAll: true,
).then((value) {
if (value.isNotEmpty == true) { if (value.isNotEmpty == true) {
return value.singleWhere((element) => element.isAll); return value.singleWhere((element) => element.isAll);
} }
@@ -167,36 +169,38 @@ class _MediaListViewState extends State<MediaListView> {
} }
} }
/// ImageProvider implementation
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> { class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
/// Constructor for creating a [MediaThumbnailProvider]
const MediaThumbnailProvider({ const MediaThumbnailProvider({
required this.media, required this.media,
}); });
/// Media to load
final AssetEntity media; final AssetEntity media;
@override @override
ImageStreamCompleter load(key, decode) { ImageStreamCompleter load(
return MultiFrameImageStreamCompleter( MediaThumbnailProvider key, DecoderCallback decode) =>
MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: 1.0, scale: 1,
informationCollector: () sync* { informationCollector: () sync* {
yield ErrorDescription('Id: ${media.id}'); yield ErrorDescription('Id: ${media.id}');
}, },
); );
}
Future<ui.Codec> _loadAsync( Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key, DecoderCallback decode) async { MediaThumbnailProvider key, DecoderCallback decode) async {
assert(key == this); assert(key == this, 'Checks MediaThumbnailProvider');
final bytes = await media.thumbData; final bytes = await media.thumbData;
return await decode(bytes!); return decode(bytes!);
} }
@override @override
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) { Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) =>
return SynchronousFuture<MediaThumbnailProvider>(this); SynchronousFuture<MediaThumbnailProvider>(this);
}
@override @override
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
@@ -1,10 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// This widget is used for showing user tiles for mentions /// This widget is used for showing user tiles for mentions
/// Use [title], [subtitle], [leading], [trailing] for substituting widgets in respective positions /// Use [title], [subtitle], [leading], [trailing] for
/// substituting widgets in respective positions
class MentionTile extends StatelessWidget { class MentionTile extends StatelessWidget {
/// Constructor for creating a [MentionTile] widget
const MentionTile(
this.member, {
Key? key,
this.title,
this.subtitle,
this.leading,
this.trailing,
}) : super(key: key);
/// Member to display in the tile /// Member to display in the tile
final Member member; final Member member;
@@ -20,36 +31,26 @@ class MentionTile extends StatelessWidget {
/// Widget at the end of tile /// Widget at the end of tile
final Widget? trailing; final Widget? trailing;
MentionTile(
this.member, {
this.title,
this.subtitle,
this.leading,
this.trailing,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => SizedBox(
return Container( height: 56,
height: 56.0,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( const SizedBox(
width: 16.0, width: 16,
), ),
leading ?? leading ??
UserAvatar( UserAvatar(
constraints: BoxConstraints.tight( constraints: BoxConstraints.tight(
Size( const Size(
40, 40,
40, 40,
), ),
), ),
user: member.user!, user: member.user!,
), ),
SizedBox( const SizedBox(
width: 8.0, width: 8,
), ),
Expanded( Expanded(
child: Align( child: Align(
@@ -60,13 +61,13 @@ class MentionTile extends StatelessWidget {
children: [ children: [
title ?? title ??
Text( Text(
'${member.user!.name}', member.user!.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: StreamChatTheme.of(context).textTheme.bodyBold, style: StreamChatTheme.of(context).textTheme.bodyBold,
), ),
SizedBox( const SizedBox(
height: 2.0, height: 2,
), ),
subtitle ?? subtitle ??
Text( Text(
@@ -88,8 +89,8 @@ class MentionTile extends StatelessWidget {
trailing ?? trailing ??
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
right: 18.0, right: 18,
left: 8.0, left: 8,
), ),
child: StreamSvgIcon.mentions( child: StreamSvgIcon.mentions(
color: StreamChatTheme.of(context).colorTheme.accentBlue, color: StreamChatTheme.of(context).colorTheme.accentBlue,
@@ -99,4 +100,3 @@ class MentionTile extends StatelessWidget {
), ),
); );
} }
}
@@ -3,6 +3,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Class describing a message action /// Class describing a message action
class MessageAction { class MessageAction {
/// returns a new instance of a [MessageAction]
MessageAction({
this.leading,
this.title,
this.onTap,
});
/// leading widget /// leading widget
final Widget? leading; final Widget? leading;
@@ -11,11 +18,4 @@ class MessageAction {
/// callback called on tap /// callback called on tap
final OnMessageTap? onTap; final OnMessageTap? onTap;
/// returns a new instance of a [MessageAction]
MessageAction({
this.leading,
this.title,
this.onTap,
});
} }
@@ -8,37 +8,11 @@ import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'extension.dart'; /// Constructs a modal with actions for a message
import 'message_input.dart';
import 'message_widget.dart';
import 'stream_chat.dart';
import 'stream_chat_theme.dart';
class MessageActionsModal extends StatefulWidget { class MessageActionsModal extends StatefulWidget {
final Widget Function(BuildContext, Message)? editMessageInputBuilder; /// Constructor for creating a [MessageActionsModal] widget
final OnMessageTap? onThreadReplyTap;
final OnMessageTap? onReplyTap;
final Message message;
final MessageTheme messageTheme;
final bool showReactions;
final OnMessageTap? onCopyTap;
final bool showDeleteMessage;
final bool showCopyMessage;
final bool showEditMessage;
final bool showResendMessage;
final bool showReplyMessage;
final bool showThreadReplyMessage;
final bool showFlagButton;
final bool reverse;
final ShapeBorder? messageShape;
final ShapeBorder? attachmentShape;
final DisplayWidget showUserAvatar;
final BorderRadius? attachmentBorderRadiusGeometry;
/// List of custom actions
final List<MessageAction> customActions;
const MessageActionsModal({ const MessageActionsModal({
Key? key, Key? key,
required this.message, required this.message,
@@ -63,6 +37,66 @@ class MessageActionsModal extends StatefulWidget {
this.onCopyTap, this.onCopyTap,
}) : super(key: key); }) : super(key: key);
/// Builder for edit message
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
/// Callback for when thread reply is tapped
final OnMessageTap? onThreadReplyTap;
/// Callback for when reply is tapped
final OnMessageTap? onReplyTap;
/// Message in focus for actions
final Message message;
/// [MessageTheme] for message
final MessageTheme messageTheme;
/// Flag for showing reactions
final bool showReactions;
/// Callback when copy is tapped
final OnMessageTap? onCopyTap;
/// Callback when delete is tapped
final bool showDeleteMessage;
/// Flag for showing copy action
final bool showCopyMessage;
/// Flag for showing edit action
final bool showEditMessage;
/// Flag for showing resend action
final bool showResendMessage;
/// Flag for showing reply action
final bool showReplyMessage;
/// Flag for showing thread reply action
final bool showThreadReplyMessage;
/// Flag for showing flag action
final bool showFlagButton;
/// Flag for reversing message
final bool reverse;
/// [ShapeBorder] to apply to the widget
final ShapeBorder? messageShape;
/// [ShapeBorder] to apply to attachment
final ShapeBorder? attachmentShape;
/// Enum for displaying user avatar
final DisplayWidget showUserAvatar;
/// [BorderRadius] for attachment border
final BorderRadius? attachmentBorderRadiusGeometry;
/// List of custom actions
final List<MessageAction> customActions;
@override @override
_MessageActionsModalState createState() => _MessageActionsModalState(); _MessageActionsModalState createState() => _MessageActionsModalState();
} }
@@ -71,9 +105,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
bool _showActions = true; bool _showActions = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => _showMessageOptionsModal();
return _showMessageOptionsModal();
}
Widget _showMessageOptionsModal() { Widget _showMessageOptionsModal() {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -119,16 +151,15 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
if (_showActions) if (_showActions)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
builder: (context, val, snapshot) { builder: (context, val, snapshot) => Transform.scale(
return Transform.scale(
scale: val, scale: val,
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Column( child: Column(
crossAxisAlignment: widget.reverse crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end ? CrossAxisAlignment.end
@@ -146,27 +177,27 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
: (divFactor > 1.0 : (divFactor > 1.0
? 0.0 ? 0.0
: -(1.0 - divFactor)), : -(1.0 - divFactor)),
0.0), 0),
child: ReactionPicker( child: ReactionPicker(
message: widget.message, message: widget.message,
), ),
), ),
SizedBox(height: 8), const SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: MessageWidget(
key: Key('MessageWidget'), key: const Key('MessageWidget'),
reverse: widget.reverse, reverse: widget.reverse,
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry, widget.attachmentBorderRadiusGeometry,
message: widget.message.copyWith( message: widget.message.copyWith(
text: widget.message.text!.length > 200 text: widget.message.text!.length > 200
// ignore: lines_longer_than_80_chars
? '${widget.message.text!.substring(0, 200)}...' ? '${widget.message.text!.substring(0, 200)}...'
: widget.message.text, : widget.message.text,
), ),
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showThreadReplyIndicator: false,
showReplyMessage: false, showReplyMessage: false,
showUserAvatar: widget.showUserAvatar, showUserAvatar: widget.showUserAvatar,
attachmentPadding: EdgeInsets.all( attachmentPadding: EdgeInsets.all(
@@ -176,22 +207,20 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
translateUserAvatar: false, translateUserAvatar: false,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: widget.message.text!.isOnlyEmoji horizontal:
? 0 widget.message.text!.isOnlyEmoji ? 0 : 16.0,
: 16.0,
), ),
showReactionPickerIndicator: showReactionPickerIndicator:
widget.showReactions && widget.showReactions &&
(widget.message.status == (widget.message.status ==
MessageSendingStatus.sent), MessageSendingStatus.sent),
showInChannelIndicator: false,
showSendingIndicator: false, showSendingIndicator: false,
shape: widget.messageShape, shape: widget.messageShape,
attachmentShape: widget.attachmentShape, attachmentShape: widget.attachmentShape,
), ),
), ),
SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: widget.reverse ? 0 : 40, left: widget.reverse ? 0 : 40,
@@ -229,12 +258,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
_buildFlagButton(context), _buildFlagButton(context),
if (widget.showDeleteMessage) if (widget.showDeleteMessage)
_buildDeleteButton(context), _buildDeleteButton(context),
...widget.customActions.map((action) { ...widget.customActions
return _buildCustomAction( .map((action) => _buildCustomAction(
context, context,
action, action,
); ))
})
].insertBetween( ].insertBetween(
Container( Container(
height: 1, height: 1,
@@ -252,8 +280,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
), ),
); ),
},
), ),
], ],
), ),
@@ -263,23 +290,22 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
InkWell _buildCustomAction( InkWell _buildCustomAction(
BuildContext context, BuildContext context,
MessageAction messageAction, MessageAction messageAction,
) { ) =>
return InkWell( InkWell(
onTap: () { onTap: () {
messageAction.onTap?.call(widget.message); messageAction.onTap?.call(widget.message);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
messageAction.leading ?? Offstage(), messageAction.leading ?? const Offstage(),
const SizedBox(width: 16), const SizedBox(width: 16),
messageAction.title ?? Offstage(), messageAction.title ?? const Offstage(),
], ],
), ),
), ),
); );
}
void _showFlagDialog() async { void _showFlagDialog() async {
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
@@ -289,9 +315,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
title: 'Flag Message', title: 'Flag Message',
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0, size: 24,
), ),
question: question:
// ignore: lines_longer_than_80_chars
'Do you want to send a copy of this message to a\nmoderator for further investigation?', 'Do you want to send a copy of this message to a\nmoderator for further investigation?',
okText: 'FLAG', okText: 'FLAG',
cancelText: 'CANCEL', cancelText: 'CANCEL',
@@ -305,7 +332,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
context, context,
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: theme.colorTheme.accentRed, color: theme.colorTheme.accentRed,
size: 24.0, size: 24,
), ),
details: 'The message has been reported to a moderator.', details: 'The message has been reported to a moderator.',
title: 'Message flagged', title: 'Message flagged',
@@ -317,7 +344,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
context, context,
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: theme.colorTheme.accentRed, color: theme.colorTheme.accentRed,
size: 24.0, size: 24,
), ),
details: 'The message has been reported to a moderator.', details: 'The message has been reported to a moderator.',
title: 'Message flagged', title: 'Message flagged',
@@ -339,7 +366,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
title: 'Delete message', title: 'Delete message',
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0, size: 24,
), ),
question: 'Are you sure you want to permanently delete this\nmessage?', question: 'Are you sure you want to permanently delete this\nmessage?',
okText: 'DELETE', okText: 'DELETE',
@@ -365,7 +392,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
context, context,
icon: StreamSvgIcon.error( icon: StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0, size: 24,
), ),
details: 'The operation couldn\'t be completed.', details: 'The operation couldn\'t be completed.',
title: 'Something went wrong', title: 'Something went wrong',
@@ -373,8 +400,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
); );
} }
Widget _buildReplyButton(BuildContext context) { Widget _buildReplyButton(BuildContext context) => InkWell(
return InkWell(
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (widget.onReplyTap != null) { if (widget.onReplyTap != null) {
@@ -382,7 +408,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
} }
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.reply( StreamSvgIcon.reply(
@@ -397,13 +423,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
); );
}
Widget _buildFlagButton(BuildContext context) { Widget _buildFlagButton(BuildContext context) => InkWell(
return InkWell( onTap: _showFlagDialog,
onTap: () => _showFlagDialog(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.iconFlag( StreamSvgIcon.iconFlag(
@@ -418,15 +442,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
); );
}
Widget _buildDeleteButton(BuildContext context) { Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed = final isDeleteFailed =
widget.message.status == MessageSendingStatus.failed_delete; widget.message.status == MessageSendingStatus.failed_delete;
return InkWell( return InkWell(
onTap: () => _showDeleteDialog(), onTap: _showDeleteDialog,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.delete( StreamSvgIcon.delete(
@@ -446,14 +469,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
); );
} }
Widget _buildCopyButton(BuildContext context) { Widget _buildCopyButton(BuildContext context) => InkWell(
return InkWell(
onTap: () async { onTap: () async {
widget.onCopyTap?.call(widget.message); widget.onCopyTap?.call(widget.message);
Navigator.pop(context); Navigator.pop(context);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.copy( StreamSvgIcon.copy(
@@ -469,16 +491,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
); );
}
Widget _buildEditMessage(BuildContext context) { Widget _buildEditMessage(BuildContext context) => InkWell(
return InkWell(
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(context);
_showEditBottomSheet(context); _showEditBottomSheet(context);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.edit( StreamSvgIcon.edit(
@@ -493,7 +513,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
); );
}
Widget _buildResendMessage(BuildContext context) { Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed = final isUpdateFailed =
@@ -509,7 +528,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
} }
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.circleUp( StreamSvgIcon.circleUp(
@@ -535,14 +554,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
isScrollControlled: true, isScrollControlled: true,
backgroundColor: backgroundColor:
StreamChatTheme.of(context).messageInputTheme.inputBackground, StreamChatTheme.of(context).messageInputTheme.inputBackground,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
), ),
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: channel, channel: channel,
child: Flex( child: Flex(
direction: Axis.vertical, direction: Axis.vertical,
@@ -555,14 +573,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: StreamSvgIcon.edit( child: StreamSvgIcon.edit(
color: StreamChatTheme.of(context) color:
.colorTheme StreamChatTheme.of(context).colorTheme.greyGainsboro,
.greyGainsboro,
), ),
), ),
Text( const Text(
'Edit Message', 'Edit Message',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
@@ -574,9 +591,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
], ],
), ),
), ),
widget.editMessageInputBuilder != null if (widget.editMessageInputBuilder != null)
? widget.editMessageInputBuilder!(context, widget.message) widget.editMessageInputBuilder!(context, widget.message)
: MessageInput( else
MessageInput(
editMessage: widget.message, editMessage: widget.message,
preMessageSending: (m) { preMessageSending: (m) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@@ -586,13 +604,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
], ],
), ),
); ),
},
); );
} }
Widget _buildThreadReplyButton(BuildContext context) { Widget _buildThreadReplyButton(BuildContext context) => InkWell(
return InkWell(
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (widget.onThreadReplyTap != null) { if (widget.onThreadReplyTap != null) {
@@ -600,7 +616,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
} }
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.thread( StreamSvgIcon.thread(
@@ -616,4 +632,3 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
); );
} }
}
File diff suppressed because it is too large Load Diff
@@ -12,34 +12,60 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/system_message.dart'; import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/swipeable.dart';
import '../stream_chat_flutter.dart'; /// Widget builder for message
import 'connection_status_builder.dart';
import 'date_divider.dart';
import 'extension.dart';
import 'swipeable.dart';
typedef MessageBuilder = Widget Function( typedef MessageBuilder = Widget Function(
BuildContext, BuildContext,
MessageDetails, MessageDetails,
List<Message>, List<Message>,
); );
/// Widget builder for parent message
typedef ParentMessageBuilder = Widget Function( typedef ParentMessageBuilder = Widget Function(
BuildContext, BuildContext,
Message?, Message?,
); );
/// Widget builder for system message
typedef SystemMessageBuilder = Widget Function( typedef SystemMessageBuilder = Widget Function(
BuildContext, BuildContext,
Message, Message,
); );
/// Widget builder for thread
typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent); typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent);
/// Callback for thread taps
typedef ThreadTapCallback = void Function(Message, Widget?); typedef ThreadTapCallback = void Function(Message, Widget?);
/// Callback on message swiped
typedef OnMessageSwiped = void Function(Message); typedef OnMessageSwiped = void Function(Message);
/// Callback on message tapped
typedef OnMessageTap = void Function(Message); typedef OnMessageTap = void Function(Message);
/// Callback on reply tapped
typedef ReplyTapCallback = void Function(Message); typedef ReplyTapCallback = void Function(Message);
/// Class for message details
class MessageDetails { class MessageDetails {
/// Constructor for creating [MessageDetails]
MessageDetails(
BuildContext context,
this.message,
List<Message> messages,
this.index,
) {
isMyMessage = message.user?.id == StreamChat.of(context).user?.id;
isLastUser = index + 1 < messages.length &&
message.user?.id == messages[index + 1].user?.id;
isNextUser =
index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id;
}
/// True if the message belongs to the current user /// True if the message belongs to the current user
bool? isMyMessage; bool? isMyMessage;
@@ -54,19 +80,6 @@ class MessageDetails {
/// The index of the message /// The index of the message
int index; int index;
MessageDetails(
BuildContext context,
this.message,
List<Message> messages,
this.index,
) {
isMyMessage = message.user?.id == StreamChat.of(context).user?.id;
isLastUser = index + 1 < messages.length &&
message.user?.id == messages[index + 1].user?.id;
isNextUser =
index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id;
}
} }
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
@@ -104,14 +117,16 @@ class MessageDetails {
/// ``` /// ```
/// ///
/// ///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels. /// Make sure to have a [StreamChannel] ancestor in order to
/// provide the information about the channels.
/// The widget uses a [ListView.custom] to render the list of channels. /// The widget uses a [ListView.custom] to render the list of channels.
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// The widget components render the ui based on the first
/// ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageListView extends StatefulWidget { class MessageListView extends StatefulWidget {
/// Instantiate a new MessageListView /// Instantiate a new MessageListView
MessageListView({ const MessageListView({
Key? key, Key? key,
this.showScrollToBottom = true, this.showScrollToBottom = true,
this.messageBuilder, this.messageBuilder,
@@ -158,10 +173,12 @@ class MessageListView extends StatefulWidget {
final ThreadBuilder? threadBuilder; final ThreadBuilder? threadBuilder;
/// Function called when tapping on a thread /// Function called when tapping on a thread
/// By default it calls [Navigator.push] using the widget built using [threadBuilder] /// By default it calls [Navigator.push] using the widget
/// built using [threadBuilder]
final ThreadTapCallback? onThreadTap; final ThreadTapCallback? onThreadTap;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero /// If true will show a scroll to bottom message when there are new
/// messages and the scroll offset is not zero
final bool showScrollToBottom; final bool showScrollToBottom;
/// Parent message in case of a thread /// Parent message in case of a thread
@@ -201,8 +218,10 @@ class MessageListView extends StatefulWidget {
/// Color used while highlighting initial message /// Color used while highlighting initial message
final Color? messageHighlightColor; final Color? messageHighlightColor;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage; final ShowMessageCallback? onShowMessage;
/// Flag for showing tile on header
final bool showConnectionStateTile; final bool showConnectionStateTile;
/// Function called when messages are fetched /// Function called when messages are fetched
@@ -214,8 +233,10 @@ class MessageListView extends StatefulWidget {
/// Function used to build an empty widget /// Function used to build an empty widget
final WidgetBuilder? emptyBuilder; final WidgetBuilder? emptyBuilder;
/// Callback triggered when an error occurs while performing the given request. /// Callback triggered when an error occurs while performing the
/// This parameter can be used to display an error message to users in the event /// given request.
/// This parameter can be used to display an error message to
/// users in the event
/// of a connection failure. /// of a connection failure.
final ErrorBuilder? errorWidgetBuilder; final ErrorBuilder? errorWidgetBuilder;
@@ -223,10 +244,12 @@ class MessageListView extends StatefulWidget {
final bool Function(Message)? messageFilter; final bool Function(Message)? messageFilter;
/// Attachment builders for the default message widget /// Attachment builders for the default message widget
/// Please change this in the [MessageWidget] if you are using a custom implementation /// Please change this in the [MessageWidget] if you are using a
/// custom implementation
final Map<String, AttachmentBuilder>? customAttachmentBuilders; final Map<String, AttachmentBuilder>? customAttachmentBuilders;
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead) /// Called when any message is tapped except a system message
/// (use [onSystemMessageTap] instead)
final OnMessageTap? onMessageTap; final OnMessageTap? onMessageTap;
/// Called when system message is tapped /// Called when system message is tapped
@@ -238,6 +261,7 @@ class MessageListView extends StatefulWidget {
/// Customize the MessageWidget textBuilder /// Customize the MessageWidget textBuilder
final void Function(BuildContext context, Message message)? textBuilder; final void Function(BuildContext context, Message message)? textBuilder;
/// Callback for when link is tapped
final void Function(String link)? onLinkTap; final void Function(String link)? onLinkTap;
@override @override
@@ -257,9 +281,8 @@ class _MessageListViewState extends State<MessageListView> {
if (streamChannel!.initialMessageId != null) { if (streamChannel!.initialMessageId != null) {
final messages = streamChannel!.channel.state!.messages; final messages = streamChannel!.channel.state!.messages;
final totalMessages = messages.length; final totalMessages = messages.length;
final messageIndex = messages.indexWhere((e) { final messageIndex =
return e.id == streamChannel!.initialMessageId; messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
});
final index = totalMessages - messageIndex; final index = totalMessages - messageIndex;
if (index != 0) return index - 1; if (index != 0) return index - 1;
return index; return index;
@@ -272,9 +295,7 @@ class _MessageListViewState extends State<MessageListView> {
return 0; return 0;
} }
bool _isInitialMessage(String id) { bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
return streamChannel!.initialMessageId == id;
}
bool get _upToDate => streamChannel!.channel.state!.isUpToDate; bool get _upToDate => streamChannel!.channel.state!.isUpToDate;
@@ -295,50 +316,46 @@ class _MessageListViewState extends State<MessageListView> {
final MessageListController _messageListController = MessageListController(); final MessageListController _messageListController = MessageListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => MessageListCore(
return MessageListCore(
messageFilter: widget.messageFilter, messageFilter: widget.messageFilter,
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) { (context) => const Center(
return Center( child: CircularProgressIndicator(),
child: const CircularProgressIndicator(), ),
);
},
emptyBuilder: widget.emptyBuilder ?? emptyBuilder: widget.emptyBuilder ??
(context) { (context) => Center(
return Center(
child: Text( child: Text(
'No chats here yet...', 'No chats here yet...',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith( style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .black
.withOpacity(.5)), .withOpacity(.5)),
), ),
); ),
},
messageListBuilder: widget.messageListBuilder ?? messageListBuilder: widget.messageListBuilder ??
(context, list) { (context, list) => _buildListView(list),
return _buildListView(list);
},
messageListController: _messageListController, messageListController: _messageListController,
parentMessage: widget.parentMessage, parentMessage: widget.parentMessage,
showScrollToBottom: widget.showScrollToBottom, showScrollToBottom: widget.showScrollToBottom,
errorWidgetBuilder: widget.errorWidgetBuilder ?? errorWidgetBuilder: widget.errorWidgetBuilder ??
(BuildContext context, Object error) { (BuildContext context, Object error) => Center(
return Center(
child: Text( child: Text(
'Something went wrong', 'Something went wrong',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith( style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .black
.withOpacity(.5)), .withOpacity(.5)),
), ),
),
); );
},
);
}
Widget _buildListView(List<Message> data) { Widget _buildListView(List<Message> data) {
messages = data; messages = data;
@@ -384,6 +401,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions
showMessage: widget.showConnectionStateTile ? showStatus : false, showMessage: widget.showConnectionStateTile ? showStatus : false,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -418,7 +436,6 @@ class _MessageListViewState extends State<MessageListView> {
child: ScrollablePositionedList.separated( child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex! + initialAlignment!), key: ValueKey(initialIndex! + initialAlignment!),
itemPositionsListener: _itemPositionListener, itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0, initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0, initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
@@ -427,8 +444,8 @@ class _MessageListViewState extends State<MessageListView> {
itemCount: itemCount:
messages.length + 2 + (_isThreadConversation ? 1 : 0), messages.length + 2 + (_isThreadConversation ? 1 : 0),
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
if (i == messages.length) return Offstage(); if (i == messages.length) return const Offstage();
if (i == 0) return SizedBox(height: 30); if (i == 0) return const SizedBox(height: 30);
if (i == messages.length + 1) { if (i == messages.length + 1) {
final replyCount = widget.parentMessage!.replyCount; final replyCount = widget.parentMessage!.replyCount;
return Container( return Container(
@@ -437,8 +454,9 @@ class _MessageListViewState extends State<MessageListView> {
StreamChatTheme.of(context).colorTheme.bgGradient, StreamChatTheme.of(context).colorTheme.bgGradient,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Text( child: Text(
// ignore: lines_longer_than_80_chars
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -464,7 +482,7 @@ class _MessageListViewState extends State<MessageListView> {
dateTime: nextMessage.createdAt.toLocal(), dateTime: nextMessage.createdAt.toLocal(),
); );
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0), padding: const EdgeInsets.symmetric(vertical: 12),
child: divider, child: divider,
); );
} }
@@ -482,9 +500,9 @@ class _MessageListViewState extends State<MessageListView> {
!isNextUserSame || !isNextUserSame ||
isThread || isThread ||
isDeleted) { isDeleted) {
return SizedBox(height: 8); return const SizedBox(height: 8);
} }
return SizedBox(height: 2); return const SizedBox(height: 2);
}, },
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == messages.length + 2) { if (i == messages.length + 2) {
@@ -554,19 +572,19 @@ class _MessageListViewState extends State<MessageListView> {
), ),
if (widget.showScrollToBottom) _buildScrollToBottom(), if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned( Positioned(
top: 20.0, top: 20,
child: ValueListenableBuilder<Iterable<ItemPosition>>( child: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: _itemPositionListener.itemPositions, valueListenable: _itemPositionListener.itemPositions,
builder: (context, values, _) { builder: (context, values, _) {
final items = _itemPositionListener.itemPositions.value; final items = _itemPositionListener.itemPositions.value;
if (items.isEmpty || messages.isEmpty) { if (items.isEmpty || messages.isEmpty) {
return SizedBox(); return const SizedBox();
} }
var index = _getTopElement(values).index; var index = _getTopElement(values).index;
if (index > messages.length) { if (index > messages.length) {
return SizedBox(); return const SizedBox();
} }
if (index == messages.length) { if (index == messages.length) {
@@ -588,19 +606,15 @@ class _MessageListViewState extends State<MessageListView> {
} }
Future<void> _paginateData( Future<void> _paginateData(
StreamChannelState? channel, QueryDirection direction) { StreamChannelState? channel, QueryDirection direction) =>
return _messageListController.paginateData!(direction: direction); _messageListController.paginateData!(direction: direction);
}
ItemPosition _getTopElement(Iterable<ItemPosition> values) { ItemPosition _getTopElement(Iterable<ItemPosition> values) => values
return values
.where((ItemPosition position) => position.itemLeadingEdge < 0.9) .where((ItemPosition position) => position.itemLeadingEdge < 0.9)
.reduce((ItemPosition max, ItemPosition position) => .reduce((ItemPosition max, ItemPosition position) =>
position.itemLeadingEdge > max.itemLeadingEdge ? position : max); position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
}
Widget _buildScrollToBottom() { Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
return StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2( stream: Rx.combineLatest2(
streamChannel!.channel.state!.isUpToDateStream, streamChannel!.channel.state!.isUpToDateStream,
streamChannel!.channel.state!.unreadCountStream, streamChannel!.channel.state!.unreadCountStream,
@@ -608,14 +622,14 @@ class _MessageListViewState extends State<MessageListView> {
), ),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Offstage(); return const Offstage();
} else if (!snapshot.hasData) { } else if (!snapshot.hasData) {
return Offstage(); return const Offstage();
} }
final isUpToDate = snapshot.data!.item1; final isUpToDate = snapshot.data!.item1;
final showScrollToBottom = !isUpToDate || _showScrollToBottom; final showScrollToBottom = !isUpToDate || _showScrollToBottom;
if (!showScrollToBottom) { if (!showScrollToBottom) {
return Offstage(); return const Offstage();
} }
final unreadCount = snapshot.data!.item2; final unreadCount = snapshot.data!.item2;
final showUnreadCount = unreadCount > 0 && final showUnreadCount = unreadCount > 0 &&
@@ -643,7 +657,7 @@ class _MessageListViewState extends State<MessageListView> {
setState(() => _showScrollToBottom = false); setState(() => _showScrollToBottom = false);
_scrollController!.scrollTo( _scrollController!.scrollTo(
index: 0, index: 0,
duration: Duration(seconds: 1), duration: const Duration(seconds: 1),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} }
@@ -660,10 +674,10 @@ class _MessageListViewState extends State<MessageListView> {
top: -10, top: -10,
child: CircleAvatar( child: CircleAvatar(
child: Padding( child: Padding(
padding: const EdgeInsets.all(3.0), padding: const EdgeInsets.all(3),
child: Text( child: Text(
'$unreadCount', '$unreadCount',
style: TextStyle( style: const TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -676,7 +690,6 @@ class _MessageListViewState extends State<MessageListView> {
); );
}, },
); );
}
Widget _buildLoadingIndicator( Widget _buildLoadingIndicator(
StreamChannelState? streamChannel, StreamChannelState? streamChannel,
@@ -686,7 +699,7 @@ class _MessageListViewState extends State<MessageListView> {
? streamChannel!.queryTopMessages ? streamChannel!.queryTopMessages
: streamChannel!.queryBottomMessages; : streamChannel!.queryBottomMessages;
return StreamBuilder<bool>( return StreamBuilder<bool>(
key: Key('LOADING-INDICATOR'), key: const Key('LOADING-INDICATOR'),
stream: stream, stream: stream,
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -696,24 +709,24 @@ class _MessageListViewState extends State<MessageListView> {
.colorTheme .colorTheme
.accentRed .accentRed
.withOpacity(.2), .withOpacity(.2),
child: Center( child: const Center(
child: Text('Error loading messages'), child: Text('Error loading messages'),
), ),
); );
} }
if (!snapshot.data!) { if (!snapshot.data!) {
if (!_isThreadConversation && direction == QueryDirection.top) { if (!_isThreadConversation && direction == QueryDirection.top) {
return Container( return const SizedBox(
height: 52, height: 52,
width: double.infinity, width: double.infinity,
); );
} }
return Offstage(); return const Offstage();
} }
return Center( return const Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8),
child: const CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); );
}, },
@@ -729,7 +742,7 @@ class _MessageListViewState extends State<MessageListView> {
Widget messageWidget; Widget messageWidget;
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
messageWidget = Builder( messageWidget = Builder(
key: ValueKey<String>('TOP-MESSAGE'), key: const ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder!( builder: (_) => widget.messageBuilder!(
context, context,
MessageDetails( MessageDetails(
@@ -799,8 +812,6 @@ class _MessageListViewState extends State<MessageListView> {
final isOnlyEmoji = message.text!.isOnlyEmoji; final isOnlyEmoji = message.text!.isOnlyEmoji;
return MessageWidget( return MessageWidget(
showThreadReplyIndicator: false,
showInChannelIndicator: false,
showReplyMessage: false, showReplyMessage: false,
showResendMessage: false, showResendMessage: false,
showThreadReplyMessage: false, showThreadReplyMessage: false,
@@ -810,17 +821,17 @@ class _MessageListViewState extends State<MessageListView> {
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showUsername: !isMyMessage, showUsername: !isMyMessage,
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
showSendingIndicator: false, showSendingIndicator: false,
onThreadTap: _onThreadTap as void Function(Message)?, onThreadTap: _onThreadTap as void Function(Message)?,
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
bottomLeft: Radius.circular(2), bottomLeft: Radius.circular(2),
topRight: Radius.circular(16), topRight: Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16),
), ),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: isOnlyEmoji ? 0 : 16.0, horizontal: isOnlyEmoji ? 0 : 16.0,
), ),
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
@@ -889,8 +900,8 @@ class _MessageListViewState extends State<MessageListView> {
final channel = streamChannel!.channel; final channel = streamChannel!.channel;
final readList = channel.state?.read?.where((read) { final readList = channel.state?.read?.where((read) {
if (read.user.id == userId) return false; if (read.user.id == userId) return false;
return (read.lastRead.isAfter(message.createdAt) || return read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt)); read.lastRead.isAtSameMomentAs(message.createdAt);
}).toList() ?? }).toList() ??
[]; [];
@@ -940,7 +951,7 @@ class _MessageListViewState extends State<MessageListView> {
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showReactions: !message.isDeleted, showReactions: !message.isDeleted,
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8),
showInChannelIndicator: showInChannelIndicator, showInChannelIndicator: showInChannelIndicator,
showThreadReplyIndicator: showThreadReplyIndicator, showThreadReplyIndicator: showThreadReplyIndicator,
showUsername: showUsername, showUsername: showUsername,
@@ -948,6 +959,7 @@ class _MessageListViewState extends State<MessageListView> {
showSendingIndicator: showSendingIndicator, showSendingIndicator: showSendingIndicator,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
onQuotedMessageTap: (quotedMessageId) async { onQuotedMessageTap: (quotedMessageId) async {
// ignore: prefer_function_declarations_over_variables
final scrollToIndex = () { final scrollToIndex = () {
final index = messages.indexWhere((m) => m.id == quotedMessageId); final index = messages.indexWhere((m) => m.id == quotedMessageId);
_scrollController?.scrollTo( _scrollController?.scrollTo(
@@ -987,17 +999,17 @@ class _MessageListViewState extends State<MessageListView> {
), ),
attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2), attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: const Radius.circular(16),
bottomLeft: Radius.circular( bottomLeft: Radius.circular(
(timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) (timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage)
? 0 ? 0
: 16, : 16,
), ),
topRight: Radius.circular(16), topRight: const Radius.circular(16),
bottomRight: Radius.circular(16), bottomRight: const Radius.circular(16),
), ),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: isOnlyEmoji ? 0 : 16.0, horizontal: isOnlyEmoji ? 0 : 16.0,
), ),
messageTheme: isMyMessage messageTheme: isMyMessage
@@ -1034,7 +1046,7 @@ class _MessageListViewState extends State<MessageListView> {
!message.isEphemeral && !message.isEphemeral &&
widget.onMessageSwiped != null) { widget.onMessageSwiped != null) {
child = Container( child = Container(
decoration: BoxDecoration(), decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: Swipeable( child: Swipeable(
onSwipeEnd: () { onSwipeEnd: () {
@@ -1062,14 +1074,12 @@ class _MessageListViewState extends State<MessageListView> {
), ),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
onEnd: () => initialMessageHighlightComplete = true, onEnd: () => initialMessageHighlightComplete = true,
builder: (_, color, child) { builder: (_, color, child) => Container(
return Container(
color: color, color: color,
child: child, child: child,
); ),
},
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.only(top: 4),
child: child, child: child,
), ),
); );
@@ -1127,19 +1137,18 @@ class _MessageListViewState extends State<MessageListView> {
_onThreadTap = (Message message) { _onThreadTap = (Message message) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (_) { MaterialPageRoute(
return StreamBuilder<Message>( builder: (_) => StreamBuilder<Message>(
stream: streamChannel!.channel.state!.messagesStream.map( stream: streamChannel!.channel.state!.messagesStream.map(
(messages) => (messages) =>
messages!.firstWhere((m) => m.id == message.id)), messages!.firstWhere((m) => m.id == message.id)),
initialData: message, initialData: message,
builder: (_, snapshot) { builder: (_, snapshot) => StreamChannel(
return StreamChannel(
channel: streamChannel!.channel, channel: streamChannel!.channel,
child: widget.threadBuilder!(context, snapshot.data), child: widget.threadBuilder!(context, snapshot.data),
); ),
}); ),
}), ),
); );
}; };
} }
@@ -7,22 +7,11 @@ import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'extension.dart'; /// Modal widget for displaying message reactions
import 'message_widget.dart';
import 'stream_chat_theme.dart';
class MessageReactionsModal extends StatelessWidget { class MessageReactionsModal extends StatelessWidget {
final Message message; /// Constructor for creating a [MessageReactionsModal] reactions
final MessageTheme messageTheme;
final bool reverse;
final bool showReactions;
final DisplayWidget showUserAvatar;
final ShapeBorder? messageShape;
final ShapeBorder? attachmentShape;
final void Function(User)? onUserAvatarTap;
final BorderRadius? attachmentBorderRadiusGeometry;
const MessageReactionsModal({ const MessageReactionsModal({
Key? key, Key? key,
required this.message, required this.message,
@@ -36,6 +25,33 @@ class MessageReactionsModal extends StatelessWidget {
this.attachmentBorderRadiusGeometry, this.attachmentBorderRadiusGeometry,
}) : super(key: key); }) : super(key: key);
/// Message to display reactions of
final Message message;
/// [MessageTheme] to apply to [message]
final MessageTheme messageTheme;
/// Flag to reverse message
final bool reverse;
/// Flag to show reactions on message
final bool showReactions;
/// Enum to change user avatar config
final DisplayWidget showUserAvatar;
/// [ShapeBorder] to apply to message
final ShapeBorder? messageShape;
/// [ShapeBorder] to apply to attachment
final ShapeBorder? attachmentShape;
/// Callback when user avatar is tapped
final void Function(User)? onUserAvatarTap;
/// [BorderRadius] to apply to attachments
final BorderRadius? attachmentBorderRadiusGeometry;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -59,8 +75,8 @@ class MessageReactionsModal extends StatelessWidget {
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return TweenAnimationBuilder<double>( return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
builder: (context, val, snapshot) { builder: (context, val, snapshot) {
final hasFileAttachment = final hasFileAttachment =
@@ -86,7 +102,7 @@ class MessageReactionsModal extends StatelessWidget {
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -102,7 +118,7 @@ class MessageReactionsModal extends StatelessWidget {
: (divFactor > 1.0 : (divFactor > 1.0
? 0.0 ? 0.0
: -(1.0 - divFactor)), : -(1.0 - divFactor)),
0.0), 0),
child: ReactionPicker( child: ReactionPicker(
message: message, message: message,
), ),
@@ -110,7 +126,7 @@ class MessageReactionsModal extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: MessageWidget(
key: Key('MessageWidget'), key: const Key('MessageWidget'),
reverse: reverse, reverse: reverse,
message: message.copyWith( message: message.copyWith(
text: message.text!.length > 200 text: message.text!.length > 200
@@ -121,7 +137,6 @@ class MessageReactionsModal extends StatelessWidget {
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showThreadReplyIndicator: false,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: false, showSendingIndicator: false,
@@ -133,9 +148,8 @@ class MessageReactionsModal extends StatelessWidget {
attachmentPadding: EdgeInsets.all( attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2, hasFileAttachment ? 4 : 2,
), ),
showInChannelIndicator: false,
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8.0, vertical: 8,
horizontal: horizontal:
message.text!.isOnlyEmoji ? 0 : 16.0, message.text!.isOnlyEmoji ? 0 : 16.0,
), ),
@@ -184,7 +198,6 @@ class MessageReactionsModal extends StatelessWidget {
child: Wrap( child: Wrap(
spacing: 16, spacing: 16,
runSpacing: 16, runSpacing: 16,
alignment: WrapAlignment.start,
children: message.latestReactions! children: message.latestReactions!
.map((e) => _buildReaction( .map((e) => _buildReaction(
e, e,
@@ -208,14 +221,12 @@ class MessageReactionsModal extends StatelessWidget {
) { ) {
final isCurrentUser = reaction.user?.id == currentUser.id; final isCurrentUser = reaction.user?.id == currentUser.id;
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints.loose(Size( constraints: BoxConstraints.loose(const Size(
64, 64,
98, 98,
)), )),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
@@ -223,11 +234,11 @@ class MessageReactionsModal extends StatelessWidget {
UserAvatar( UserAvatar(
onTap: onUserAvatarTap, onTap: onUserAvatarTap,
user: reaction.user!, user: reaction.user!,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 64, height: 64,
width: 64, width: 64,
), ),
onlineIndicatorConstraints: BoxConstraints.tightFor( onlineIndicatorConstraints: const BoxConstraints.tightFor(
height: 12, height: 12,
width: 12, width: 12,
), ),
@@ -5,9 +5,11 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// It shows the current [Message] preview. /// It shows the current [Message] preview.
/// ///
/// Usually you don't use this widget as it's the default item used by [MessageSearchListView]. /// Usually you don't use this widget as it's the default item used by
/// [MessageSearchListView].
/// ///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageSearchItem extends StatelessWidget { class MessageSearchItem extends StatelessWidget {
/// Instantiate a new MessageSearchItem /// Instantiate a new MessageSearchItem
@@ -38,7 +40,7 @@ class MessageSearchItem extends StatelessWidget {
leading: UserAvatar( leading: UserAvatar(
user: user, user: user,
showOnlineStatus: showOnlineStatus, showOnlineStatus: showOnlineStatus,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
@@ -69,7 +71,7 @@ class MessageSearchItem extends StatelessWidget {
subtitle: Row( subtitle: Row(
children: [ children: [
Expanded(child: _buildSubtitle(context, message)), Expanded(child: _buildSubtitle(context, message)),
SizedBox(width: 16), const SizedBox(width: 16),
_buildDate(context, message), _buildDate(context, message),
], ],
), ),
@@ -147,9 +149,9 @@ class MessageSearchItem extends StatelessWidget {
List<Attachment> attachments, List<Attachment> attachments,
TextStyle? normalTextStyle, TextStyle? normalTextStyle,
TextStyle? mentionsTextStyle) { TextStyle? mentionsTextStyle) {
var textList = text.split(' '); final textList = text.split(' ');
var resList = <TextSpan>[]; final resList = <TextSpan>[];
for (var e in textList) { for (final e in textList) {
if (mentions.isNotEmpty && if (mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) { mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan( resList.add(TextSpan(
@@ -166,7 +168,7 @@ class MessageSearchItem extends StatelessWidget {
)); ));
} else { } else {
resList.add(TextSpan( resList.add(TextSpan(
text: e == textList.last ? '$e' : '$e ', text: e == textList.last ? e : '$e ',
style: normalTextStyle, style: normalTextStyle,
)); ));
} }
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart';
/// Callback called when tapping on a user /// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
@@ -43,10 +42,12 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// ``` /// ```
/// ///
/// ///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages. /// Make sure to have a [MessageSearchBloc] ancestor in order to provide the
/// information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages. /// The widget uses a [ListView.separated] to render the list of messages.
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget { class MessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView /// Instantiate a new MessageSearchListView
@@ -78,8 +79,10 @@ class MessageSearchListView extends StatefulWidget {
final Filter filters; final Filter filters;
/// The sorting used for the channels matching the filters. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// 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. /// Direction can be ascending or descending.
final List<SortOption>? sortOptions; final List<SortOption>? sortOptions;
@@ -109,6 +112,7 @@ class MessageSearchListView extends StatefulWidget {
/// Set it to false to disable the pull-to-refresh widget /// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh; final bool pullToRefresh;
/// Show error tile on top
final bool showErrorTile; final bool showErrorTile;
/// The builder that is used when the search messages are fetched /// The builder that is used when the search messages are fetched
@@ -132,8 +136,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
MessageSearchListController(); MessageSearchListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => MessageSearchListCore(
return MessageSearchListCore(
filters: widget.filters, filters: widget.filters,
sortOptions: widget.sortOptions, sortOptions: widget.sortOptions,
messageQuery: widget.messageQuery, messageQuery: widget.messageQuery,
@@ -141,23 +144,20 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
messageFilters: widget.messageFilters, messageFilters: widget.messageFilters,
messageSearchListController: _messageSearchListController, messageSearchListController: _messageSearchListController,
emptyBuilder: widget.emptyBuilder ?? emptyBuilder: widget.emptyBuilder ??
(context) { (context) => LayoutBuilder(
return LayoutBuilder( builder: (context, viewportConstraints) =>
builder: (context, viewportConstraints) { SingleChildScrollView(
return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(),
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Center( child: const Center(
child: Text('There are no messages currently'), child: Text('There are no messages currently'),
), ),
), ),
); ),
}, ),
);
},
errorBuilder: widget.errorBuilder ?? errorBuilder: widget.errorBuilder ??
(BuildContext context, dynamic error) { (BuildContext context, dynamic error) {
if (error is Error) { if (error is Error) {
@@ -172,36 +172,27 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
); );
}, },
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) { (context) => LayoutBuilder(
return LayoutBuilder( builder: (context, viewportConstraints) =>
builder: (context, viewportConstraints) { SingleChildScrollView(
return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(),
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
),
),
childBuilder: widget.childBuilder ?? _buildListView,
); );
},
);
},
childBuilder: widget.childBuilder ??
(list) {
return _buildListView(list);
},
);
}
Widget _separatorBuilder(BuildContext context, int index) { Widget _separatorBuilder(BuildContext context, int index) => Container(
return Container(
height: 1, height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
); );
}
Widget _listItemBuilder( Widget _listItemBuilder(
BuildContext context, GetMessageResponse getMessageResponse) { BuildContext context, GetMessageResponse getMessageResponse) {
@@ -227,8 +218,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
.colorTheme .colorTheme
.accentRed .accentRed
.withOpacity(.2), .withOpacity(.2),
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: Text('Error loading messages'), child: Text('Error loading messages'),
), ),
@@ -237,9 +228,11 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
} }
return Container( return Container(
height: 100, height: 100,
padding: EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Center( child: Center(
child: snapshot.data! ? CircularProgressIndicator() : Container(), child: snapshot.data!
? const CircularProgressIndicator()
: Container(),
), ),
); );
}); });
@@ -249,7 +242,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
final items = data; final items = data;
Widget child = ListView.separated( Widget child = ListView.separated(
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length, itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) { separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) { if (widget.separatorBuilder != null) {
@@ -2,11 +2,11 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_chat_theme.dart'; /// Text widget to display in message
import 'utils.dart';
class MessageText extends StatelessWidget { class MessageText extends StatelessWidget {
/// Constructor for creating a [MessageText] widget
const MessageText({ const MessageText({
Key? key, Key? key,
required this.message, required this.message,
@@ -15,9 +15,16 @@ class MessageText extends StatelessWidget {
this.onLinkTap, this.onLinkTap,
}) : super(key: key); }) : super(key: key);
/// Message whose text is to be displayed
final Message message; final Message message;
/// Callback for when mention is tapped
final void Function(User)? onMentionTap; final void Function(User)? onMentionTap;
/// Callback for when link is tapped
final void Function(String)? onLinkTap; final void Function(String)? onLinkTap;
/// [MessageTheme] whose text theme is to be applied
final MessageTheme messageTheme; final MessageTheme messageTheme;
@override @override
@@ -71,6 +78,7 @@ class MessageText extends StatelessWidget {
String _replaceMentions(String text) { String _replaceMentions(String text) {
message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) { message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) {
// ignore: parameter_assignments
text = text.replaceAll( text = text.replaceAll(
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})'); '@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
}); });
@@ -15,17 +15,17 @@ import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/image_group.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'attachment/attachment.dart'; /// Widget builder for building attachments
import 'extension.dart';
import 'image_group.dart';
import 'message_text.dart';
typedef AttachmentBuilder = Widget Function( typedef AttachmentBuilder = Widget Function(
BuildContext, BuildContext,
Message, Message,
List<Attachment>, List<Attachment>,
); );
/// Callback for when quoted message is tapped
typedef OnQuotedMessageTap = void Function(String?); typedef OnQuotedMessageTap = void Function(String?);
/// The display behaviour of a widget /// The display behaviour of a widget
@@ -45,119 +45,13 @@ enum DisplayWidget {
/// ///
/// It shows a message with reactions, replies and user avatar. /// It shows a message with reactions, replies and user avatar.
/// ///
/// Usually you don't use this widget as it's the default message widget used by [MessageListView]. /// Usually you don't use this widget as it's the default message widget used by
/// [MessageListView].
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageWidget extends StatefulWidget { class MessageWidget extends StatefulWidget {
/// Function called on mention tap
final void Function(User)? onMentionTap;
/// The function called when tapping on replies
final void Function(Message)? onThreadTap;
final void Function(Message)? onReplyTap;
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
final Widget Function(BuildContext, Message)? textBuilder;
/// Function called on long press
final void Function(BuildContext, Message)? onMessageActions;
/// The message
final Message message;
/// The message theme
final MessageTheme messageTheme;
/// If true the widget will be mirrored
final bool reverse;
/// The shape of the message text
final ShapeBorder? shape;
/// The shape of an attachment
final ShapeBorder? attachmentShape;
/// The borderside of the message text
final BorderSide? borderSide;
/// The borderside of an attachment
final BorderSide? attachmentBorderSide;
/// The border radius of the message text
final BorderRadiusGeometry? borderRadiusGeometry;
/// The border radius of an attachment
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
/// The padding of the widget
final EdgeInsetsGeometry? padding;
/// The internal padding of the message text
final EdgeInsets textPadding;
/// The internal padding of an attachment
final EdgeInsetsGeometry attachmentPadding;
/// It controls the display behaviour of the user avatar
final DisplayWidget showUserAvatar;
/// It controls the display behaviour of the sending indicator
final bool showSendingIndicator;
/// If true the widget will show the reactions
final bool showReactions;
final bool allRead;
/// If true the widget will show the thread reply indicator
final bool showThreadReplyIndicator;
/// If true the widget will show the show in channel indicator
final bool showInChannelIndicator;
/// The function called when tapping on UserAvatar
final void Function(User)? onUserAvatarTap;
/// The function called when tapping on a link
final void Function(String)? onLinkTap;
/// Used in [MessageReactionsModal] and [MessageActionsModal]
final bool showReactionPickerIndicator;
final List<Read>? readList;
final ShowMessageCallback? onShowMessage;
final ValueChanged<ReturnActionType>? onReturnAction;
/// If true show the users username next to the timestamp of the message
final bool showUsername;
final bool showTimestamp;
final bool showReplyMessage;
final bool showThreadReplyMessage;
final bool showEditMessage;
final bool showCopyMessage;
final bool showDeleteMessage;
final bool showResendMessage;
final bool showFlagButton;
final Map<String, AttachmentBuilder> attachmentBuilders;
/// Center user avatar with bottom of the message
final bool translateUserAvatar;
/// Function called when quotedMessage is tapped
final OnQuotedMessageTap? onQuotedMessageTap;
/// Function called when message is tapped
final void Function(Message)? onMessageTap;
/// List of custom actions shown on message long tap
final List<MessageAction> customActions;
// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// ///
MessageWidget({ MessageWidget({
Key? key, Key? key,
@@ -201,8 +95,8 @@ class MessageWidget extends StatefulWidget {
this.readList, this.readList,
this.padding, this.padding,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16,
vertical: 8.0, vertical: 8,
), ),
this.attachmentPadding = EdgeInsets.zero, this.attachmentPadding = EdgeInsets.zero,
this.allRead = false, this.allRead = false,
@@ -211,8 +105,7 @@ class MessageWidget extends StatefulWidget {
this.onAttachmentTap, this.onAttachmentTap,
}) : attachmentBuilders = { }) : attachmentBuilders = {
'image': (context, message, attachments) { 'image': (context, message, attachments) {
var border = RoundedRectangleBorder( final border = RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
); );
@@ -267,16 +160,15 @@ class MessageWidget extends StatefulWidget {
); );
}, },
'video': (context, message, attachments) { 'video': (context, message, attachments) {
var border = RoundedRectangleBorder( final border = RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
); );
return wrapAttachmentWidget( return wrapAttachmentWidget(
context, context,
Column( Column(
children: attachments.map((attachment) { children: attachments
return VideoAttachment( .map((attachment) => VideoAttachment(
attachment: attachment, attachment: attachment,
messageTheme: messageTheme, messageTheme: messageTheme,
size: Size( size: Size(
@@ -291,8 +183,8 @@ class MessageWidget extends StatefulWidget {
onAttachmentTap(message, attachment); onAttachmentTap(message, attachment);
} }
: null, : null,
); ))
}).toList(), .toList(),
), ),
border, border,
reverse, reverse,
@@ -302,15 +194,14 @@ class MessageWidget extends StatefulWidget {
}, },
'giphy': (context, message, attachments) { 'giphy': (context, message, attachments) {
final border = RoundedRectangleBorder( final border = RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
); );
return wrapAttachmentWidget( return wrapAttachmentWidget(
context, context,
Column( Column(
children: attachments.map((attachment) { children: attachments
return GiphyAttachment( .map((attachment) => GiphyAttachment(
attachment: attachment, attachment: attachment,
message: message, message: message,
size: Size( size: Size(
@@ -319,8 +210,8 @@ class MessageWidget extends StatefulWidget {
), ),
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
onReturnAction: onReturnAction, onReturnAction: onReturnAction,
); ))
}).toList(), .toList(),
), ),
border, border,
reverse, reverse,
@@ -329,7 +220,7 @@ class MessageWidget extends StatefulWidget {
); );
}, },
'file': (context, message, attachments) { 'file': (context, message, attachments) {
var border = RoundedRectangleBorder( final border = RoundedRectangleBorder(
side: attachmentBorderSide ?? side: attachmentBorderSide ??
BorderSide( BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
@@ -339,8 +230,7 @@ class MessageWidget extends StatefulWidget {
return Column( return Column(
children: attachments children: attachments
.map<Widget>((attachment) { .map<Widget>((attachment) => wrapAttachmentWidget(
return wrapAttachmentWidget(
context, context,
FileAttachment( FileAttachment(
message: message, message: message,
@@ -354,8 +244,7 @@ class MessageWidget extends StatefulWidget {
reverse, reverse,
attachmentBorderRadiusGeometry as BorderRadius? ?? attachmentBorderRadiusGeometry as BorderRadius? ??
BorderRadius.zero, BorderRadius.zero,
); ))
})
.insertBetween(SizedBox( .insertBetween(SizedBox(
height: attachmentPadding.vertical / 2, height: attachmentPadding.vertical / 2,
)) ))
@@ -365,6 +254,141 @@ class MessageWidget extends StatefulWidget {
}..addAll(customAttachmentBuilders ?? {}), }..addAll(customAttachmentBuilders ?? {}),
super(key: key); super(key: key);
/// Function called on mention tap
final void Function(User)? onMentionTap;
/// The function called when tapping on threads
final void Function(Message)? onThreadTap;
/// The function called when tapping on replies
final void Function(Message)? onReplyTap;
/// Widget builder for edit message layout
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
/// Widget builder for building text
final Widget Function(BuildContext, Message)? textBuilder;
/// Function called on long press
final void Function(BuildContext, Message)? onMessageActions;
/// The message
final Message message;
/// The message theme
final MessageTheme messageTheme;
/// If true the widget will be mirrored
final bool reverse;
/// The shape of the message text
final ShapeBorder? shape;
/// The shape of an attachment
final ShapeBorder? attachmentShape;
/// The borderside of the message text
final BorderSide? borderSide;
/// The borderside of an attachment
final BorderSide? attachmentBorderSide;
/// The border radius of the message text
final BorderRadiusGeometry? borderRadiusGeometry;
/// The border radius of an attachment
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
/// The padding of the widget
final EdgeInsetsGeometry? padding;
/// The internal padding of the message text
final EdgeInsets textPadding;
/// The internal padding of an attachment
final EdgeInsetsGeometry attachmentPadding;
/// It controls the display behaviour of the user avatar
final DisplayWidget showUserAvatar;
/// It controls the display behaviour of the sending indicator
final bool showSendingIndicator;
/// If true the widget will show the reactions
final bool showReactions;
///
final bool allRead;
/// If true the widget will show the thread reply indicator
final bool showThreadReplyIndicator;
/// If true the widget will show the show in channel indicator
final bool showInChannelIndicator;
/// The function called when tapping on UserAvatar
final void Function(User)? onUserAvatarTap;
/// The function called when tapping on a link
final void Function(String)? onLinkTap;
/// Used in [MessageReactionsModal] and [MessageActionsModal]
final bool showReactionPickerIndicator;
/// List of users who read
final List<Read>? readList;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Handle return actions like reply message
final ValueChanged<ReturnActionType>? onReturnAction;
/// If true show the users username next to the timestamp of the message
final bool showUsername;
/// Show message timestamp
final bool showTimestamp;
/// Show reply action
final bool showReplyMessage;
/// Show thread reply action
final bool showThreadReplyMessage;
/// Show edit action
final bool showEditMessage;
/// Show copy action
final bool showCopyMessage;
/// Show delete action
final bool showDeleteMessage;
/// Show resend action
final bool showResendMessage;
/// Show flag action
final bool showFlagButton;
/// Builder for respective attachment types
final Map<String, AttachmentBuilder> attachmentBuilders;
/// Center user avatar with bottom of the message
final bool translateUserAvatar;
/// Function called when quotedMessage is tapped
final OnQuotedMessageTap? onQuotedMessageTap;
/// Function called when message is tapped
final void Function(Message)? onMessageTap;
/// List of custom actions shown on message long tap
final List<MessageAction> customActions;
/// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap;
@override @override
_MessageWidgetState createState() => _MessageWidgetState(); _MessageWidgetState createState() => _MessageWidgetState();
} }
@@ -426,7 +450,7 @@ class _MessageWidgetState extends State<MessageWidget>
super.build(context); super.build(context);
final avatarWidth = final avatarWidth =
widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40;
var leftPadding = final leftPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
return Material( return Material(
@@ -440,7 +464,7 @@ class _MessageWidgetState extends State<MessageWidget>
? null ? null
: () => onLongPress(context), : () => onLongPress(context),
child: Padding( child: Padding(
padding: widget.padding ?? EdgeInsets.all(8), padding: widget.padding ?? const EdgeInsets.all(8),
child: Transform( child: Transform(
alignment: Alignment.center, alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
@@ -460,7 +484,6 @@ class _MessageWidgetState extends State<MessageWidget>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@@ -468,7 +491,7 @@ class _MessageWidgetState extends State<MessageWidget>
DisplayWidget.show && DisplayWidget.show &&
widget.message.user != null) ...[ widget.message.user != null) ...[
_buildUserAvatar(), _buildUserAvatar(),
SizedBox(width: 4), const SizedBox(width: 4),
], ],
if (widget.showUserAvatar == DisplayWidget.hide) if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(width: avatarWidth + 4), SizedBox(width: avatarWidth + 4),
@@ -477,12 +500,12 @@ class _MessageWidgetState extends State<MessageWidget>
portal: Container( portal: Container(
transform: transform:
Matrix4.translationValues(-12, 0, 0), Matrix4.translationValues(-12, 0, 0),
constraints: constraints: const BoxConstraints(
BoxConstraints(maxWidth: 22 * 6.0), maxWidth: 22 * 6.0),
child: _buildReactionIndicator(context), child: _buildReactionIndicator(context),
), ),
portalAnchor: Alignment(-1.0, -1.0), portalAnchor: const Alignment(-1, -1),
childAnchor: Alignment(1, -1.0), childAnchor: const Alignment(1, -1),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -505,15 +528,18 @@ class _MessageWidgetState extends State<MessageWidget>
transform: Matrix4.rotationY( transform: Matrix4.rotationY(
widget.reverse ? pi : 0), widget.reverse ? pi : 0),
child: Container( child: Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: horizontal:
// ignore: lines_longer_than_80_chars
widget.showUserAvatar == widget.showUserAvatar ==
DisplayWidget // ignore: lines_longer_than_80_chars
.gone DisplayWidget.gone
? 0 ? 0
: 4.0), : 4.0),
child: DeletedMessage( child: DeletedMessage(
reverse: widget.reverse, reverse: widget.reverse,
// ignore: lines_longer_than_80_chars
borderRadiusGeometry: widget borderRadiusGeometry: widget
.borderRadiusGeometry, .borderRadiusGeometry,
borderSide: borderSide:
@@ -526,11 +552,12 @@ class _MessageWidgetState extends State<MessageWidget>
) )
: Card( : Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
elevation: 0.0, elevation: 0,
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: (isFailedState horizontal: (isFailedState
? 15.0 ? 15.0
: 0.0) + : 0.0) +
// ignore: lines_longer_than_80_chars
(widget.showUserAvatar == (widget.showUserAvatar ==
DisplayWidget DisplayWidget
.gone .gone
@@ -543,11 +570,14 @@ class _MessageWidgetState extends State<MessageWidget>
.borderSide ?? .borderSide ??
BorderSide( BorderSide(
color: widget color: widget
// ignore: lines_longer_than_80_chars
.messageTheme .messageTheme
// ignore: lines_longer_than_80_chars
.messageBorderColor ?? .messageBorderColor ??
Colors.grey, Colors.grey,
), ),
borderRadius: widget borderRadius: widget
// ignore: lines_longer_than_80_chars
.borderRadiusGeometry ?? .borderRadiusGeometry ??
BorderRadius.zero, BorderRadius.zero,
), ),
@@ -650,9 +680,9 @@ class _MessageWidgetState extends State<MessageWidget>
children: [ children: [
StreamSvgIcon.eye( StreamSvgIcon.eye(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.grey,
size: 16.0, size: 16,
), ),
SizedBox(width: 8.0), const SizedBox(width: 8),
Text( Text(
'Only visible to you', 'Only visible to you',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -665,7 +695,7 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
var children = <Widget>[]; final children = <Widget>[];
final threadParticipants = widget.message.threadParticipants?.take(2); final threadParticipants = widget.message.threadParticipants?.take(2);
final showThreadParticipants = threadParticipants?.isNotEmpty == true; final showThreadParticipants = threadParticipants?.isNotEmpty == true;
@@ -676,6 +706,7 @@ class _MessageWidgetState extends State<MessageWidget>
msg = '$replyCount Thread Replies'; msg = '$replyCount Thread Replies';
} }
// ignore: prefer_function_declarations_over_variables
final onThreadTap = () async { final onThreadTap = () async {
try { try {
var message = widget.message; var message = widget.message;
@@ -687,6 +718,7 @@ class _MessageWidgetState extends State<MessageWidget>
} catch (e, stk) { } catch (e, stk) {
print(e); print(e);
print(stk); print(stk);
// ignore: avoid_returning_null_for_void
return null; return null;
} }
}; };
@@ -734,7 +766,7 @@ class _MessageWidgetState extends State<MessageWidget>
((widget.messageTheme.replies?.fontSize ?? 1) / 2), ((widget.messageTheme.replies?.fontSize ?? 1) / 2),
), ),
child: CustomPaint( child: CustomPaint(
size: Size(16, 32) * context.textScaleFactor, size: const Size(16, 32) * context.textScaleFactor,
painter: _ThreadReplyPainter( painter: _ThreadReplyPainter(
context: context, context: context,
color: widget.messageTheme.messageBorderColor, color: widget.messageTheme.messageBorderColor,
@@ -746,7 +778,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget mappedChild = Transform( Widget mappedChild = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Container( child: SizedBox(
height: context.textScaleFactor * 14, height: context.textScaleFactor * 14,
child: child, child: child,
), ),
@@ -757,18 +789,18 @@ class _MessageWidgetState extends State<MessageWidget>
return mappedChild; return mappedChild;
}, },
), ),
].insertBetween(const SizedBox(width: 8.0)), ].insertBetween(const SizedBox(width: 8)),
); );
} }
Widget _buildUrlAttachment() { Widget _buildUrlAttachment() {
var urlAttachment = widget.message.attachments final urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null); .firstWhere((element) => element.ogScrapeUrl != null);
var host = Uri.parse(urlAttachment.ogScrapeUrl!).host; final host = Uri.parse(urlAttachment.ogScrapeUrl!).host;
var splitList = host.split('.'); final splitList = host.split('.');
var hostName = splitList.length == 3 ? splitList[1] : splitList[0]; final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
var hostDisplayName = urlAttachment.authorName?.capitalize() ?? final hostDisplayName = urlAttachment.authorName?.capitalize() ??
getWebsiteName(hostName.toLowerCase()) ?? getWebsiteName(hostName.toLowerCase()) ??
hostName.capitalize(); hostName.capitalize();
@@ -796,7 +828,7 @@ class _MessageWidgetState extends State<MessageWidget>
padding: const EdgeInsets.all(1), padding: const EdgeInsets.all(1),
child: UserAvatar( child: UserAvatar(
user: user, user: user,
constraints: BoxConstraints.loose(Size.fromRadius(7)), constraints: BoxConstraints.loose(const Size.fromRadius(7)),
showOnlineStatus: false, showOnlineStatus: false,
), ),
), ),
@@ -820,7 +852,7 @@ class _MessageWidgetState extends State<MessageWidget>
..sort((a, b) => a.user!.id == ownId ? 1 : -1); ..sort((a, b) => a.user!.id == ownId ? 1 : -1);
return AnimatedSwitcher( return AnimatedSwitcher(
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: (widget.showReactions && child: (widget.showReactions &&
(widget.message.reactionCounts?.isNotEmpty == true) && (widget.message.reactionCounts?.isNotEmpty == true) &&
!widget.message.isDeleted) !widget.message.isDeleted)
@@ -839,7 +871,7 @@ class _MessageWidgetState extends State<MessageWidget>
reactions: reactionsList, reactions: reactionsList,
), ),
) )
: SizedBox(), : const SizedBox(),
); );
} }
@@ -849,8 +881,7 @@ class _MessageWidgetState extends State<MessageWidget>
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: MessageActionsModal(
onCopyTap: (message) => onCopyTap: (message) =>
@@ -863,16 +894,16 @@ class _MessageWidgetState extends State<MessageWidget>
: DisplayWidget.show, : DisplayWidget.show,
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
messageShape: widget.shape ?? _getDefaultShape(context), messageShape: widget.shape ?? _getDefaultShape(context),
attachmentShape: attachmentShape: widget.attachmentShape ??
widget.attachmentShape ?? _getDefaultAttachmentShape(context), _getDefaultAttachmentShape(context),
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
onThreadReplyTap: widget.onThreadTap, onThreadReplyTap: widget.onThreadTap,
showResendMessage: showResendMessage: widget.showResendMessage &&
widget.showResendMessage && (isSendFailed || isUpdateFailed), (isSendFailed || isUpdateFailed),
showCopyMessage: widget.showCopyMessage && showCopyMessage: widget.showCopyMessage &&
!isFailedState && !isFailedState &&
widget.message.text?.trim().isNotEmpty == true, widget.message.text?.trim().isNotEmpty == true,
@@ -891,8 +922,7 @@ class _MessageWidgetState extends State<MessageWidget>
showFlagButton: widget.showFlagButton, showFlagButton: widget.showFlagButton,
customActions: widget.customActions, customActions: widget.customActions,
), ),
); ));
});
} }
void _showMessageReactionsModalBottomSheet(BuildContext context) { void _showMessageReactionsModalBottomSheet(BuildContext context) {
@@ -900,8 +930,7 @@ class _MessageWidgetState extends State<MessageWidget>
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) => StreamChannel(
return StreamChannel(
channel: channel, channel: channel,
child: MessageReactionsModal( child: MessageReactionsModal(
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry:
@@ -919,8 +948,8 @@ class _MessageWidgetState extends State<MessageWidget>
message: widget.message, message: widget.message,
showReactions: widget.showReactions, showReactions: widget.showReactions,
), ),
),
); );
});
} }
ShapeBorder _getDefaultAttachmentShape(BuildContext context) { ShapeBorder _getDefaultAttachmentShape(BuildContext context) {
@@ -937,15 +966,13 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
ShapeBorder _getDefaultShape(BuildContext context) { ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
return RoundedRectangleBorder(
side: widget.borderSide ?? side: widget.borderSide ??
BorderSide( BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
), ),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
); );
}
Widget _parseAttachments() { Widget _parseAttachments() {
final attachmentGroups = <String, List<Attachment>>{}; final attachmentGroups = <String, List<Attachment>>{};
@@ -1007,9 +1034,8 @@ class _MessageWidgetState extends State<MessageWidget>
(message.status == MessageSendingStatus.sending || (message.status == MessageSendingStatus.sending ||
message.status == MessageSendingStatus.updating)) { message.status == MessageSendingStatus.updating)) {
final totalAttachments = message.attachments.length; final totalAttachments = message.attachments.length;
final uploadRemaining = message.attachments.where((it) { final uploadRemaining =
return !it.uploadState.isSuccess; message.attachments.where((it) => !it.uploadState.isSuccess).length;
}).length;
if (uploadRemaining == 0) { if (uploadRemaining == 0) {
return StreamSvgIcon.check( return StreamSvgIcon.check(
size: style!.fontSize, size: style!.fontSize,
@@ -1037,7 +1063,7 @@ class _MessageWidgetState extends State<MessageWidget>
color: StreamChatTheme.of(context).colorTheme.accentBlue, color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
SizedBox(width: 2), const SizedBox(width: 2),
child, child,
], ],
); );
@@ -1068,7 +1094,7 @@ class _MessageWidgetState extends State<MessageWidget>
); );
Widget _buildTextBubble() { Widget _buildTextBubble() {
if (widget.message.text!.trim().isEmpty) return Offstage(); if (widget.message.text!.trim().isEmpty) return const Offstage();
return Transform( return Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
@@ -1139,11 +1165,11 @@ class _MessageWidgetState extends State<MessageWidget>
} }
class _ThreadReplyPainter extends CustomPainter { class _ThreadReplyPainter extends CustomPainter {
const _ThreadReplyPainter({this.context, required this.color});
final Color? color; final Color? color;
final BuildContext? context; final BuildContext? context;
const _ThreadReplyPainter({this.context, required this.color});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = Paint() final paint = Paint()
@@ -1,17 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// List tile for [ChannelBottomSheet]
class OptionListTile extends StatelessWidget { class OptionListTile extends StatelessWidget {
final String? title; /// Constructor for creating [OptionListTile]
final Widget? leading; const OptionListTile({
final Widget? trailing; Key? key,
final VoidCallback? onTap;
final Color? titleColor;
final Color? tileColor;
final Color? separatorColor;
final TextStyle? titleTextStyle;
OptionListTile({
this.title, this.title,
this.leading, this.leading,
this.trailing, this.trailing,
@@ -20,29 +14,52 @@ class OptionListTile extends StatelessWidget {
this.tileColor, this.tileColor,
this.separatorColor, this.separatorColor,
this.titleTextStyle, this.titleTextStyle,
}); }) : super(key: key);
/// Title for tile
final String? title;
/// Leading widget (start)
final Widget? leading;
/// Trailing widget (end)
final Widget? trailing;
/// Callback when tile is tapped
final VoidCallback? onTap;
/// Title color
final Color? titleColor;
/// Background tile color
final Color? tileColor;
/// Separator color
final Color? separatorColor;
/// [TextStyle] to apply to [title]
final TextStyle? titleTextStyle;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Column(
return Column(
children: [ children: [
Container( Container(
color: separatorColor ?? color: separatorColor ??
StreamChatTheme.of(context).colorTheme.greyGainsboro, StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 1.0, height: 1,
), ),
Material( Material(
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white, color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
child: Container( child: SizedBox(
height: 63.0, height: 63,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Row( child: Row(
children: [ children: [
if (leading != null) Center(child: leading), if (leading != null) Center(child: leading),
if (leading == null) if (leading == null)
SizedBox( const SizedBox(
width: 16.0, width: 16,
), ),
Expanded( Expanded(
flex: 4, flex: 4,
@@ -62,7 +79,7 @@ class OptionListTile extends StatelessWidget {
Expanded( Expanded(
flex: 2, flex: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: trailing ?? Container(), child: trailing ?? Container(),
@@ -77,4 +94,3 @@ class OptionListTile extends StatelessWidget {
], ],
); );
} }
}
@@ -4,29 +4,25 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'attachment/attachment.dart'; /// Widget builder for quoted message attachment thumnail
import 'extension.dart';
import 'message_text.dart';
import 'stream_chat_theme.dart';
import 'user_avatar.dart';
import 'utils.dart';
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
BuildContext, BuildContext,
Attachment, Attachment,
); );
class _VideoAttachmentThumbnail extends StatefulWidget { class _VideoAttachmentThumbnail extends StatefulWidget {
final Size size;
final Attachment attachment;
const _VideoAttachmentThumbnail({ const _VideoAttachmentThumbnail({
Key? key, Key? key,
required this.attachment, required this.attachment,
this.size = const Size(32, 32), this.size = const Size(32, 32),
}) : super(key: key); }) : super(key: key);
final Size size;
final Attachment attachment;
@override @override
_VideoAttachmentThumbnailState createState() => _VideoAttachmentThumbnailState createState() =>
_VideoAttachmentThumbnailState(); _VideoAttachmentThumbnailState();
@@ -51,18 +47,30 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => SizedBox(
return Container(
height: widget.size.height, height: widget.size.height,
width: widget.size.width, width: widget.size.width,
child: _controller.value.isInitialized child: _controller.value.isInitialized
? VideoPlayer(_controller) ? VideoPlayer(_controller)
: CircularProgressIndicator()); : const CircularProgressIndicator(),
} );
} }
/// ///
class QuotedMessageWidget extends StatelessWidget { class QuotedMessageWidget extends StatelessWidget {
///
const QuotedMessageWidget({
Key? key,
required this.message,
required this.messageTheme,
this.reverse = false,
this.showBorder = false,
this.textLimit = 170,
this.attachmentThumbnailBuilders,
this.padding = const EdgeInsets.all(8),
this.onTap,
}) : super(key: key);
/// The message /// The message
final Message message; final Message message;
@@ -82,23 +90,12 @@ class QuotedMessageWidget extends StatelessWidget {
final Map<String, QuotedMessageAttachmentThumbnailBuilder>? final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
attachmentThumbnailBuilders; attachmentThumbnailBuilders;
/// Padding around the widget
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// Callback for tap on widget
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
///
QuotedMessageWidget({
Key? key,
required this.message,
required this.messageTheme,
this.reverse = false,
this.showBorder = false,
this.textLimit = 170,
this.attachmentThumbnailBuilders,
this.padding = const EdgeInsets.all(8),
this.onTap,
}) : super(key: key);
bool get _hasAttachments => message.attachments.isNotEmpty == true; bool get _hasAttachments => message.attachments.isNotEmpty == true;
bool get _containsScrapeUrl => bool get _containsScrapeUrl =>
@@ -107,8 +104,7 @@ class QuotedMessageWidget extends StatelessWidget {
bool get _containsText => message.text?.isNotEmpty == true; bool get _containsText => message.text?.isNotEmpty == true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Padding(
return Padding(
padding: padding, padding: padding,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
@@ -117,13 +113,12 @@ class QuotedMessageWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible(child: _buildMessage(context)), Flexible(child: _buildMessage(context)),
SizedBox(width: 8), const SizedBox(width: 8),
if (message.user != null) _buildUserAvatar(), if (message.user != null) _buildUserAvatar(),
], ],
), ),
), ),
); );
}
Widget _buildMessage(BuildContext context) { Widget _buildMessage(BuildContext context) {
final isOnlyEmoji = message.text!.isOnlyEmoji; final isOnlyEmoji = message.text!.isOnlyEmoji;
@@ -165,7 +160,7 @@ class QuotedMessageWidget extends StatelessWidget {
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
) )
: null, : null,
borderRadius: BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(12), topRight: Radius.circular(12),
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12), bottomLeft: Radius.circular(12),
@@ -183,7 +178,7 @@ class QuotedMessageWidget extends StatelessWidget {
} }
Widget _buildUrlAttachment(Attachment attachment) { Widget _buildUrlAttachment(Attachment attachment) {
final size = Size(32, 32); const size = Size(32, 32);
if (attachment.thumbUrl != null) { if (attachment.thumbUrl != null) {
return Container( return Container(
height: size.height, height: size.height,
@@ -198,7 +193,7 @@ class QuotedMessageWidget extends StatelessWidget {
), ),
); );
} }
return AttachmentError(size: size); return const AttachmentError(size: size);
} }
Widget _parseAttachments(BuildContext context) { Widget _parseAttachments(BuildContext context) {
@@ -217,7 +212,7 @@ class QuotedMessageWidget extends StatelessWidget {
} }
attachmentBuilder = _defaultAttachmentBuilder[attachment.type]; attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
if (attachmentBuilder == null) { if (attachmentBuilder == null) {
child = Offstage(); child = const Offstage();
} else { } else {
child = attachmentBuilder(context, attachment); child = attachmentBuilder(context, attachment);
} }
@@ -235,77 +230,63 @@ class QuotedMessageWidget extends StatelessWidget {
); );
} }
ShapeBorder _getDefaultShape(BuildContext context) { ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
return RoundedRectangleBorder( side: const BorderSide(width: 0, color: Colors.transparent),
side: BorderSide(width: 0.0, color: Colors.transparent),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
); );
}
Widget _buildUserAvatar() { Widget _buildUserAvatar() => Transform(
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0), transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: UserAvatar( child: UserAvatar(
user: message.user!, user: message.user!,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 24, height: 24,
width: 24, width: 24,
), ),
showOnlineStatus: false, showOnlineStatus: false,
), ),
); );
}
Map<String, QuotedMessageAttachmentThumbnailBuilder> Map<String, QuotedMessageAttachmentThumbnailBuilder>
get _defaultAttachmentBuilder { get _defaultAttachmentBuilder => {
return { 'image': (_, attachment) => ImageAttachment(
'image': (_, attachment) {
return ImageAttachment(
attachment: attachment, attachment: attachment,
message: message, message: message,
messageTheme: messageTheme, messageTheme: messageTheme,
size: Size(32, 32), size: const Size(32, 32),
); ),
}, 'video': (_, attachment) => _VideoAttachmentThumbnail(
'video': (_, attachment) {
return _VideoAttachmentThumbnail(
key: ValueKey(attachment.assetUrl), key: ValueKey(attachment.assetUrl),
attachment: attachment, attachment: attachment,
); ),
},
'giphy': (_, attachment) { 'giphy': (_, attachment) {
final size = Size(32, 32); const size = Size(32, 32);
return CachedNetworkImage( return CachedNetworkImage(
height: size.height, height: size.height,
width: size.width, width: size.width,
placeholder: (_, __) { placeholder: (_, __) => SizedBox(
return Container(
width: size.width, width: size.width,
height: size.height, height: size.height,
child: Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); ),
},
imageUrl: attachment.thumbUrl ?? imageUrl: attachment.thumbUrl ??
attachment.imageUrl ?? attachment.imageUrl ??
attachment.assetUrl!, attachment.assetUrl!,
errorWidget: (context, url, error) { errorWidget: (context, url, error) =>
return AttachmentError(size: size); const AttachmentError(size: size),
},
fit: BoxFit.cover, fit: BoxFit.cover,
); );
}, },
'file': (_, attachment) { 'file': (_, attachment) => SizedBox(
return Container(
height: 32, height: 32,
width: 32, width: 32,
child: getFileTypeImage(attachment.extraData['mime_type'] as String?), child: getFileTypeImage(
); attachment.extraData['mime_type'] as String?),
}, ),
}; };
}
Color? _getBackgroundColor(BuildContext context) { Color? _getBackgroundColor(BuildContext context) {
if (_containsScrapeUrl) { if (_containsScrapeUrl) {
@@ -8,7 +8,9 @@ import 'package:stream_chat_flutter/src/reaction_icon.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Creates reaction bubble widget for displaying over messages
class ReactionBubble extends StatelessWidget { class ReactionBubble extends StatelessWidget {
/// Constructor for creating a [ReactionBubble]
const ReactionBubble({ const ReactionBubble({
Key? key, Key? key,
required this.reactions, required this.reactions,
@@ -21,13 +23,28 @@ class ReactionBubble extends StatelessWidget {
this.tailCirclesSpacing = 0, this.tailCirclesSpacing = 0,
}) : super(key: key); }) : super(key: key);
/// Reactions to show
final List<Reaction> reactions; final List<Reaction> reactions;
/// Border color of bubble
final Color borderColor; final Color borderColor;
/// Background color of bubble
final Color backgroundColor; final Color backgroundColor;
/// Mask color
final Color maskColor; final Color maskColor;
/// Reverse for other side
final bool reverse; final bool reverse;
/// Reverse tail for other side
final bool flipTail; final bool flipTail;
/// Flag for highlighting own reactions
final bool highlightOwnReactions; final bool highlightOwnReactions;
/// Spacing for tail circles
final double tailCirclesSpacing; final double tailCirclesSpacing;
@override @override
@@ -40,7 +57,6 @@ class ReactionBubble extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
fit: StackFit.loose,
children: [ children: [
Transform.translate( Transform.translate(
offset: Offset(reverse ? offset : -offset, 0), offset: Offset(reverse ? offset : -offset, 0),
@@ -48,7 +64,7 @@ class ReactionBubble extends StatelessWidget {
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: maskColor, color: maskColor,
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
), ),
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@@ -60,35 +76,32 @@ class ReactionBubble extends StatelessWidget {
color: borderColor, color: borderColor,
), ),
color: backgroundColor, color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(14)), borderRadius: const BorderRadius.all(Radius.circular(14)),
), ),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) => Flex(
return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (constraints.maxWidth < double.infinity) if (constraints.maxWidth < double.infinity)
...reactions ...reactions
.take((constraints.maxWidth) ~/ 24) .take((constraints.maxWidth) ~/ 24)
.map((reaction) { .map((reaction) => _buildReaction(
return _buildReaction(
reactionIcons, reactionIcons,
reaction, reaction,
context, context,
); ))
}).toList(), .toList(),
if (constraints.maxWidth == double.infinity) if (constraints.maxWidth == double.infinity)
...reactions.map((reaction) { ...reactions
return _buildReaction( .map((reaction) => _buildReaction(
reactionIcons, reactionIcons,
reaction, reaction,
context, context,
); ))
}).toList(), .toList(),
], ],
); ),
},
), ),
), ),
), ),
@@ -115,7 +128,7 @@ class ReactionBubble extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 4.0, horizontal: 4,
), ),
child: reactionIcon != null child: reactionIcon != null
? StreamSvgIcon( ? StreamSvgIcon(
@@ -161,12 +174,9 @@ class ReactionBubble extends StatelessWidget {
} }
} }
/// Painter widget for a reaction bubble
class ReactionBubblePainter extends CustomPainter { class ReactionBubblePainter extends CustomPainter {
final Color color; /// Constructor for creating a [ReactionBubblePainter]
final Color borderColor;
final Color maskColor;
final double tailCirclesSpace;
ReactionBubblePainter( ReactionBubblePainter(
this.color, this.color,
this.borderColor, this.borderColor,
@@ -174,6 +184,18 @@ class ReactionBubblePainter extends CustomPainter {
this.tailCirclesSpace = 0, this.tailCirclesSpace = 0,
}); });
/// Color of bubble
final Color color;
/// Border color of bubble
final Color borderColor;
/// Mask color
final Color maskColor;
/// Tail circle space
final double tailCirclesSpace;
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
_drawOvalMask(size, canvas); _drawOvalMask(size, canvas);
@@ -194,10 +216,11 @@ class ReactionBubblePainter extends CustomPainter {
..color = maskColor ..color = maskColor
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
final path = Path(); final path = Path()
path.addOval( ..addOval(
Rect.fromCircle( Rect.fromCircle(
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), center:
const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 4, radius: 4,
), ),
); );
@@ -210,10 +233,11 @@ class ReactionBubblePainter extends CustomPainter {
..strokeWidth = 1 ..strokeWidth = 1
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final path = Path(); final path = Path()
path.addOval( ..addOval(
Rect.fromCircle( Rect.fromCircle(
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), center:
const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 2, radius: 2,
), ),
); );
@@ -225,9 +249,9 @@ class ReactionBubblePainter extends CustomPainter {
..color = color ..color = color
..strokeWidth = 1; ..strokeWidth = 1;
final path = Path(); final path = Path()
path.addOval(Rect.fromCircle( ..addOval(Rect.fromCircle(
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), center: const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
radius: 2, radius: 2,
)); ));
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
@@ -239,13 +263,13 @@ class ReactionBubblePainter extends CustomPainter {
..strokeWidth = 1 ..strokeWidth = 1
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final dy = -2.2; const dy = -2.2;
final startAngle = 1.1; const startAngle = 1.1;
final sweepAngle = 1.2; const sweepAngle = 1.2;
final path = Path(); final path = Path()
path.addArc( ..addArc(
Rect.fromCircle( Rect.fromCircle(
center: Offset(1, dy), center: const Offset(1, dy),
radius: 4, radius: 4,
), ),
-pi * startAngle, -pi * startAngle,
@@ -259,13 +283,13 @@ class ReactionBubblePainter extends CustomPainter {
..color = color ..color = color
..strokeWidth = 1; ..strokeWidth = 1;
final dy = -2.2; const dy = -2.2;
final startAngle = 1; const startAngle = 1;
final sweepAngle = 1.3; const sweepAngle = 1.3;
final path = Path(); final path = Path()
path.addArc( ..addArc(
Rect.fromCircle( Rect.fromCircle(
center: Offset(1, dy), center: const Offset(1, dy),
radius: 4, radius: 4,
), ),
-pi * startAngle, -pi * startAngle,
@@ -280,13 +304,13 @@ class ReactionBubblePainter extends CustomPainter {
..strokeWidth = 1 ..strokeWidth = 1
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
final dy = -2.2; const dy = -2.2;
final startAngle = 1.1; const startAngle = 1.1;
final sweepAngle = 1.2; const sweepAngle = 1.2;
final path = Path(); final path = Path()
path.addArc( ..addArc(
Rect.fromCircle( Rect.fromCircle(
center: Offset(1, dy), center: const Offset(1, dy),
radius: 6, radius: 6,
), ),
-pi * startAngle, -pi * startAngle,
@@ -296,7 +320,5 @@ class ReactionBubblePainter extends CustomPainter {
} }
@override @override
bool shouldRepaint(CustomPainter oldDelegate) { bool shouldRepaint(CustomPainter oldDelegate) => true;
return true;
}
} }
@@ -1,9 +1,14 @@
/// Reaction icon data
class ReactionIcon { class ReactionIcon {
final String type; /// Constructor for creating [ReactionIcon]
final String assetName;
ReactionIcon({ ReactionIcon({
required this.type, required this.type,
required this.assetName, required this.assetName,
}); });
/// Type of reaction
final String type;
/// Asset to display for reaction
final String assetName;
} }
@@ -3,23 +3,24 @@ import 'dart:math';
import 'package:ezanimation/ezanimation.dart'; import 'package:ezanimation/ezanimation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import '../stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'extension.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png)
/// ///
/// It shows a reaction picker /// It shows a reaction picker
/// ///
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions]. /// Usually you don't use this widget as it's one of the default widgets used
/// by [MessageWidget.onMessageActions].
class ReactionPicker extends StatefulWidget { class ReactionPicker extends StatefulWidget {
/// Constructor for creating a [ReactionPicker] widget
const ReactionPicker({ const ReactionPicker({
Key? key, Key? key,
required this.message, required this.message,
}) : super(key: key); }) : super(key: key);
/// Message to attach the reaction to
final Message message; final Message message;
@override @override
@@ -39,7 +40,7 @@ class _ReactionPickerState extends State<ReactionPicker>
animations.add( animations.add(
EzAnimation.tween( EzAnimation.tween(
Tween(begin: 0.0, end: 1.0), Tween(begin: 0.0, end: 1.0),
Duration(milliseconds: 500), const Duration(milliseconds: 500),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
), ),
); );
@@ -49,11 +50,10 @@ class _ReactionPickerState extends State<ReactionPicker>
} }
return TweenAnimationBuilder<double>( return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0, end: 1),
curve: Curves.easeInOutBack, curve: Curves.easeInOutBack,
duration: Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
builder: (context, val, wid) { builder: (context, val, wid) => Transform.scale(
return Transform.scale(
scale: val, scale: val,
child: Material( child: Material(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
@@ -61,8 +61,8 @@ class _ReactionPickerState extends State<ReactionPicker>
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16,
vertical: 8.0, vertical: 8,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -74,21 +74,19 @@ class _ReactionPickerState extends State<ReactionPicker>
?.indexWhere((reaction) => ?.indexWhere((reaction) =>
reaction.type == reactionIcon.type) ?? reaction.type == reactionIcon.type) ??
-1; -1;
var index = reactionIcons.indexOf(reactionIcon); final index = reactionIcons.indexOf(reactionIcon);
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 24, height: 24,
width: 24, width: 24,
), ),
child: RawMaterialButton( child: RawMaterialButton(
elevation: 0, elevation: 0,
padding: const EdgeInsets.all(0),
clipBehavior: Clip.none,
shape: ContinuousRectangleBorder( shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 24, height: 24,
width: 24, width: 24,
), ),
@@ -96,8 +94,8 @@ class _ReactionPickerState extends State<ReactionPicker>
if (ownReactionIndex != -1) { if (ownReactionIndex != -1) {
removeReaction( removeReaction(
context, context,
widget widget.message
.message.ownReactions![ownReactionIndex], .ownReactions![ownReactionIndex],
); );
} else { } else {
sendReaction( sendReaction(
@@ -108,9 +106,7 @@ class _ReactionPickerState extends State<ReactionPicker>
}, },
child: AnimatedBuilder( child: AnimatedBuilder(
animation: animations[index], animation: animations[index],
builder: (context, val) { builder: (context, val) => Transform.scale(
return Transform.scale(
alignment: Alignment.center,
scale: animations[index].value, scale: animations[index].value,
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: reactionIcon.assetName, assetName: reactionIcon.assetName,
@@ -131,31 +127,29 @@ class _ReactionPickerState extends State<ReactionPicker>
.color! .color!
.withOpacity(.5), .withOpacity(.5),
), ),
); )),
}),
), ),
); );
}) })
.insertBetween(SizedBox( .insertBetween(const SizedBox(
width: 16, width: 16,
)) ))
.toList(), .toList(),
), ),
), ),
), ),
); ));
});
} }
void triggerAnimations() async { void triggerAnimations() async {
for (var a in animations) { for (final a in animations) {
a.start(); a.start();
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
} }
} }
void pop() async { void pop() async {
for (var a in animations) { for (final a in animations) {
a.stop(); a.stop();
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -179,7 +173,7 @@ class _ReactionPickerState extends State<ReactionPicker>
@override @override
void dispose() { void dispose() {
for (var a in animations) { for (final a in animations) {
a.dispose(); a.dispose();
} }
super.dispose(); super.dispose();
@@ -3,10 +3,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Used to show the sending status of the message /// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget { class SendingIndicator extends StatelessWidget {
final Message message; /// Constructor for creating a [SendingIndicator] widget
final bool isMessageRead;
final double? size;
const SendingIndicator({ const SendingIndicator({
Key? key, Key? key,
required this.message, required this.message,
@@ -14,6 +11,15 @@ class SendingIndicator extends StatelessWidget {
this.size = 12, this.size = 12,
}) : super(key: key); }) : super(key: key);
/// Message for sending indicator
final Message message;
/// Flag if message is read
final bool isMessageRead;
/// Size for message
final double? size;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isMessageRead) { if (isMessageRead) {
@@ -35,6 +41,6 @@ class SendingIndicator extends StatelessWidget {
size: size, size: size,
); );
} }
return SizedBox(); return const SizedBox();
} }
} }
@@ -31,28 +31,33 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// ///
/// Use [StreamChat.of] to get the current [StreamChatState] instance. /// Use [StreamChat.of] to get the current [StreamChatState] instance.
class StreamChat extends StatefulWidget { class StreamChat extends StatefulWidget {
final StreamChatClient client; /// Constructor for creating a [StreamChat] widget
final Widget? child; const StreamChat({
final StreamChatThemeData? streamChatThemeData;
/// The amount of time that will pass before disconnecting the client in the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
/// is in background. Can be used to display various notifications depending
/// upon the [Event.type]
final EventHandler? onBackgroundEventReceived;
StreamChat({
Key? key, Key? key,
required this.client, required this.client,
required this.child, required this.child,
this.streamChatThemeData, this.streamChatThemeData,
this.onBackgroundEventReceived, this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1), this.backgroundKeepAlive = const Duration(minutes: 1),
}) : super( }) : super(key: key);
key: key,
); /// Client to do chat ops with
final StreamChatClient client;
/// Child which inherits details
final Widget? child;
/// Theme to pass on
final StreamChatThemeData? streamChatThemeData;
/// The amount of time that will pass before disconnecting the client
/// in the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
/// is in background. Can be used to display various notifications depending
/// upon the [Event.type]
final EventHandler? onBackgroundEventReceived;
@override @override
StreamChatState createState() => StreamChatState(); StreamChatState createState() => StreamChatState();
@@ -74,6 +79,7 @@ class StreamChat extends StatefulWidget {
/// The current state of the StreamChat widget /// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> { class StreamChatState extends State<StreamChat> {
/// Gets client from widget
StreamChatClient get client => widget.client; StreamChatClient get client => widget.client;
@override @override
@@ -96,7 +102,7 @@ class StreamChatState extends State<StreamChat> {
client: client, client: client,
onBackgroundEventReceived: widget.onBackgroundEventReceived, onBackgroundEventReceived: widget.onBackgroundEventReceived,
backgroundKeepAlive: widget.backgroundKeepAlive, backgroundKeepAlive: widget.backgroundKeepAlive,
child: widget.child ?? Offstage(), child: widget.child ?? const Offstage(),
), ),
); );
}, },
@@ -11,9 +11,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree /// Inherited widget providing the [StreamChatThemeData] to the widget tree
class StreamChatTheme extends InheritedWidget { class StreamChatTheme extends InheritedWidget {
final StreamChatThemeData data; /// Constructor for creating a [StreamChatTheme]
const StreamChatTheme({
StreamChatTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
@@ -22,10 +21,11 @@ class StreamChatTheme extends InheritedWidget {
child: child, child: child,
); );
/// Theme data
final StreamChatThemeData data;
@override @override
bool updateShouldNotify(StreamChatTheme old) { bool updateShouldNotify(StreamChatTheme old) => data != old.data;
return data != old.data;
}
/// Use this method to get the current [StreamChatThemeData] instance /// Use this method to get the current [StreamChatThemeData] instance
static StreamChatThemeData of(BuildContext context) { static StreamChatThemeData of(BuildContext context) {
@@ -43,42 +43,6 @@ class StreamChatTheme extends InheritedWidget {
/// Theme data /// Theme data
class StreamChatThemeData { class StreamChatThemeData {
/// The text themes used in the widgets
final TextTheme textTheme;
/// The color themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the [ChannelListHeader]
final ChannelListHeaderTheme channelListHeaderTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// Theme dedicated to the [MessageInput] widget
final MessageInputTheme messageInputTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
/// Primary icon theme
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
/// Create a theme from scratch /// Create a theme from scratch
factory StreamChatThemeData({ factory StreamChatThemeData({
Brightness? brightness, Brightness? brightness,
@@ -121,12 +85,15 @@ class StreamChatThemeData {
return defaultData.merge(customizedData); return defaultData.merge(customizedData);
} }
/// Theme initialised with light
factory StreamChatThemeData.light() => factory StreamChatThemeData.light() =>
StreamChatThemeData(brightness: Brightness.light); StreamChatThemeData(brightness: Brightness.light);
/// Theme initialised with dark
factory StreamChatThemeData.dark() => factory StreamChatThemeData.dark() =>
StreamChatThemeData(brightness: Brightness.dark); StreamChatThemeData(brightness: Brightness.dark);
/// Raw theme init
const StreamChatThemeData.raw({ const StreamChatThemeData.raw({
required this.textTheme, required this.textTheme,
required this.colorTheme, required this.colorTheme,
@@ -154,7 +121,44 @@ class StreamChatThemeData {
return defaultTheme.merge(customizedTheme); return defaultTheme.merge(customizedTheme);
} }
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden. /// The text themes used in the widgets
final TextTheme textTheme;
/// The color themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the [ChannelListHeader]
final ChannelListHeaderTheme channelListHeaderTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// Theme dedicated to the [MessageInput] widget
final MessageInputTheme messageInputTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
/// Primary icon theme
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
/// Creates a copy of [StreamChatThemeData] with specified attributes
/// overridden.
StreamChatThemeData copyWith({ StreamChatThemeData copyWith({
TextTheme? textTheme, TextTheme? textTheme,
ColorTheme? colorTheme, ColorTheme? colorTheme,
@@ -185,6 +189,7 @@ class StreamChatThemeData {
reactionIcons: reactionIcons ?? this.reactionIcons, reactionIcons: reactionIcons ?? this.reactionIcons,
); );
/// Merge themes
StreamChatThemeData merge(StreamChatThemeData? other) { StreamChatThemeData merge(StreamChatThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -204,6 +209,8 @@ class StreamChatThemeData {
); );
} }
/// Create theme from color and text theme
// ignore: prefer_constructors_over_static_methods
static StreamChatThemeData fromColorAndTextTheme( static StreamChatThemeData fromColorAndTextTheme(
ColorTheme colorTheme, ColorTheme colorTheme,
TextTheme textTheme, TextTheme textTheme,
@@ -213,7 +220,7 @@ class StreamChatThemeData {
textTheme: textTheme, textTheme: textTheme,
colorTheme: colorTheme, colorTheme: colorTheme,
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)), primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
defaultChannelImage: (context, channel) => SizedBox(), defaultChannelImage: (context, channel) => const SizedBox(),
defaultUserImage: (context, user) => Center( defaultUserImage: (context, user) => Center(
child: CachedNetworkImage( child: CachedNetworkImage(
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
@@ -225,23 +232,23 @@ class StreamChatThemeData {
unreadCounterColor: colorTheme.accentRed, unreadCounterColor: colorTheme.accentRed,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
), ),
title: textTheme.bodyBold, title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith( subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
lastMessageAt: textTheme.footnote.copyWith( lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5), color: colorTheme.black.withOpacity(.5),
), ),
indicatorIconSize: 16.0), indicatorIconSize: 16),
channelListHeaderTheme: ChannelListHeaderTheme( channelListHeaderTheme: ChannelListHeaderTheme(
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
@@ -253,7 +260,7 @@ class StreamChatThemeData {
channelHeaderTheme: ChannelHeaderTheme( channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
@@ -261,7 +268,7 @@ class StreamChatThemeData {
color: colorTheme.white, color: colorTheme.white,
title: textTheme.headlineBold, title: textTheme.headlineBold,
subtitle: textTheme.footnote.copyWith( subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
), ),
), ),
@@ -277,7 +284,7 @@ class StreamChatThemeData {
messageBorderColor: colorTheme.greyGainsboro, messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 32, height: 32,
width: 32, width: 32,
), ),
@@ -301,7 +308,7 @@ class StreamChatThemeData {
messageBorderColor: colorTheme.greyWhisper, messageBorderColor: colorTheme.greyWhisper,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 32, height: 32,
width: 32, width: 32,
), ),
@@ -309,7 +316,7 @@ class StreamChatThemeData {
), ),
messageInputTheme: MessageInputTheme( messageInputTheme: MessageInputTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
sendAnimationDuration: Duration(milliseconds: 300), sendAnimationDuration: const Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentBlue, actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey, actionButtonIdleColor: colorTheme.grey,
expandButtonColor: colorTheme.accentBlue, expandButtonColor: colorTheme.accentBlue,
@@ -356,16 +363,9 @@ class StreamChatThemeData {
} }
} }
/// Class for holding text theme
class TextTheme { class TextTheme {
final TextStyle title; /// Initialise light text theme
final TextStyle headlineBold;
final TextStyle headline;
final TextStyle bodyBold;
final TextStyle body;
final TextStyle footnoteBold;
final TextStyle footnote;
final TextStyle captionBold;
TextTheme.light({ TextTheme.light({
this.title = const TextStyle( this.title = const TextStyle(
fontSize: 22, fontSize: 22,
@@ -408,6 +408,7 @@ class TextTheme {
), ),
}); });
/// Initialise with dark theme
TextTheme.dark({ TextTheme.dark({
this.title = const TextStyle( this.title = const TextStyle(
fontSize: 22, fontSize: 22,
@@ -450,6 +451,31 @@ class TextTheme {
), ),
}); });
/// Text theme for title
final TextStyle title;
/// Body Text theme for headline
final TextStyle headlineBold;
/// Text theme for headline
final TextStyle headline;
/// Bold Text theme for body
final TextStyle bodyBold;
/// Text theme body
final TextStyle body;
/// Bold Text theme for footnote
final TextStyle footnoteBold;
/// Text theme for footnote
final TextStyle footnote;
/// Bold Text theme for caption
final TextStyle captionBold;
/// Copy with theme
TextTheme copyWith({ TextTheme copyWith({
Brightness brightness = Brightness.light, Brightness brightness = Brightness.light,
TextStyle? body, TextStyle? body,
@@ -460,8 +486,8 @@ class TextTheme {
TextStyle? footnoteBold, TextStyle? footnoteBold,
TextStyle? footnote, TextStyle? footnote,
TextStyle? captionBold, TextStyle? captionBold,
}) { }) =>
return brightness == Brightness.light brightness == Brightness.light
? TextTheme.light( ? TextTheme.light(
body: body ?? this.body, body: body ?? this.body,
title: title ?? this.title, title: title ?? this.title,
@@ -482,8 +508,8 @@ class TextTheme {
footnote: footnote ?? this.footnote, footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold, captionBold: captionBold ?? this.captionBold,
); );
}
/// Merge text theme
TextTheme merge(TextTheme? other) { TextTheme merge(TextTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -499,28 +525,9 @@ class TextTheme {
} }
} }
/// Theme that holds colors
class ColorTheme { class ColorTheme {
final Color black; /// Initialise with light theme
final Color grey;
final Color greyGainsboro;
final Color greyWhisper;
final Color whiteSmoke;
final Color whiteSnow;
final Color white;
final Color blueAlice;
final Color accentBlue;
final Color accentRed;
final Color accentGreen;
final Effect borderTop;
final Effect borderBottom;
final Effect shadowIconButton;
final Effect modalShadow;
final Color highlight;
final Color overlay;
final Color overlayDark;
final Gradient bgGradient;
final Brightness brightness;
ColorTheme.light({ ColorTheme.light({
this.black = const Color(0xff000000), this.black = const Color(0xff000000),
this.grey = const Color(0xff7a7a7a), this.grey = const Color(0xff7a7a7a),
@@ -543,19 +550,16 @@ class ColorTheme {
stops: [0, 1], stops: [0, 1],
), ),
this.borderTop = const Effect( this.borderTop = const Effect(
sigmaX: 0, sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08),
sigmaY: -1,
color: Color(0xff000000),
blur: 0.0,
alpha: 0.08),
this.borderBottom = const Effect( this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0, alpha: 0.08), sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08),
this.shadowIconButton = const Effect( this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0), sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4),
this.modalShadow = const Effect( this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0), sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8),
}) : brightness = Brightness.light; }) : brightness = Brightness.light;
/// Initialise with dark theme
ColorTheme.dark({ ColorTheme.dark({
this.black = const Color(0xffffffff), this.black = const Color(0xffffffff),
this.grey = const Color(0xff7a7a7a), this.grey = const Color(0xff7a7a7a),
@@ -572,28 +576,28 @@ class ColorTheme {
sigmaX: 0, sigmaX: 0,
sigmaY: -1, sigmaY: -1,
color: Color(0xff141924), color: Color(0xff141924),
blur: 0.0, blur: 0,
), ),
this.borderBottom = const Effect( this.borderBottom = const Effect(
sigmaX: 0, sigmaX: 0,
sigmaY: 1, sigmaY: 1,
color: Color(0xff141924), color: Color(0xff141924),
blur: 0.0, blur: 0,
alpha: 1.0, alpha: 1,
), ),
this.shadowIconButton = const Effect( this.shadowIconButton = const Effect(
sigmaX: 0, sigmaX: 0,
sigmaY: 2, sigmaY: 2,
color: Color(0xff000000), color: Color(0xff000000),
alpha: 0.5, alpha: 0.5,
blur: 4.0, blur: 4,
), ),
this.modalShadow = const Effect( this.modalShadow = const Effect(
sigmaX: 0, sigmaX: 0,
sigmaY: 0, sigmaY: 0,
color: Color(0xff000000), color: Color(0xff000000),
alpha: 1, alpha: 1,
blur: 8.0, blur: 8,
), ),
this.highlight = const Color(0xff302d22), this.highlight = const Color(0xff302d22),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4), this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
@@ -609,6 +613,67 @@ class ColorTheme {
), ),
}) : brightness = Brightness.dark; }) : brightness = Brightness.dark;
///
final Color black;
///
final Color grey;
///
final Color greyGainsboro;
///
final Color greyWhisper;
///
final Color whiteSmoke;
///
final Color whiteSnow;
///
final Color white;
///
final Color blueAlice;
///
final Color accentBlue;
///
final Color accentRed;
///
final Color accentGreen;
///
final Effect borderTop;
///
final Effect borderBottom;
///
final Effect shadowIconButton;
///
final Effect modalShadow;
///
final Color highlight;
///
final Color overlay;
///
final Color overlayDark;
///
final Gradient bgGradient;
///
final Brightness brightness;
/// Copy with theme
ColorTheme copyWith({ ColorTheme copyWith({
Brightness brightness = Brightness.light, Brightness brightness = Brightness.light,
Color? black, Color? black,
@@ -630,8 +695,8 @@ class ColorTheme {
Color? overlay, Color? overlay,
Color? overlayDark, Color? overlayDark,
Gradient? bgGradient, Gradient? bgGradient,
}) { }) =>
return brightness == Brightness.light brightness == Brightness.light
? ColorTheme.light( ? ColorTheme.light(
black: black ?? this.black, black: black ?? this.black,
grey: grey ?? this.grey, grey: grey ?? this.grey,
@@ -674,8 +739,8 @@ class ColorTheme {
overlayDark: overlayDark ?? this.overlayDark, overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient, bgGradient: bgGradient ?? this.bgGradient,
); );
}
/// Merge color theme
ColorTheme merge(ColorTheme? other) { ColorTheme merge(ColorTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -704,13 +769,14 @@ class ColorTheme {
/// Channel theme data /// Channel theme data
class ChannelTheme { class ChannelTheme {
/// Theme of the [ChannelHeader] widget /// Constructor for creating [ChannelTheme]
final ChannelHeaderTheme channelHeaderTheme;
ChannelTheme({ ChannelTheme({
required this.channelHeaderTheme, required this.channelHeaderTheme,
}); });
/// Theme of the [ChannelHeader] widget
final ChannelHeaderTheme channelHeaderTheme;
/// Creates a copy of [ChannelTheme] with specified attributes overridden. /// Creates a copy of [ChannelTheme] with specified attributes overridden.
ChannelTheme copyWith({ ChannelTheme copyWith({
ChannelHeaderTheme? channelHeaderTheme, ChannelHeaderTheme? channelHeaderTheme,
@@ -719,6 +785,7 @@ class ChannelTheme {
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
); );
/// Merge with theme
ChannelTheme merge(ChannelTheme? other) { ChannelTheme merge(ChannelTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -727,28 +794,30 @@ class ChannelTheme {
} }
} }
/// Theme for avatar
class AvatarTheme { class AvatarTheme {
final BoxConstraints? _constraints; /// Constructor for creating [AvatarTheme]
final BorderRadius? _borderRadius;
BoxConstraints get constraints {
return _constraints ??
BoxConstraints.tightFor(
height: 32,
width: 32,
);
}
BorderRadius get borderRadius {
return _borderRadius ?? BorderRadius.circular(20);
}
AvatarTheme({ AvatarTheme({
BoxConstraints? constraints, BoxConstraints? constraints,
BorderRadius? borderRadius, BorderRadius? borderRadius,
}) : _constraints = constraints, }) : _constraints = constraints,
_borderRadius = borderRadius; _borderRadius = borderRadius;
final BoxConstraints? _constraints;
final BorderRadius? _borderRadius;
/// Get constraints for avatar
BoxConstraints get constraints =>
_constraints ??
const BoxConstraints.tightFor(
height: 32,
width: 32,
);
/// Get border radius
BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20);
/// Copy with another theme
AvatarTheme copyWith({ AvatarTheme copyWith({
BoxConstraints? constraints, BoxConstraints? constraints,
BorderRadius? borderRadius, BorderRadius? borderRadius,
@@ -758,6 +827,7 @@ class AvatarTheme {
borderRadius: borderRadius ?? _borderRadius, borderRadius: borderRadius ?? _borderRadius,
); );
/// Merge with another AvatarTheme
AvatarTheme merge(AvatarTheme? other) { AvatarTheme merge(AvatarTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -767,19 +837,9 @@ class AvatarTheme {
} }
} }
/// Class for getting message theme
class MessageTheme { class MessageTheme {
final TextStyle? messageText; /// Constructor into [MessageTheme]
final TextStyle? messageAuthor;
final TextStyle? messageLinks;
final TextStyle? createdAt;
final TextStyle? replies;
final Color? messageBackgroundColor;
final Color? messageBorderColor;
final Color? reactionsBackgroundColor;
final Color? reactionsBorderColor;
final Color? reactionsMaskColor;
final AvatarTheme? avatarTheme;
const MessageTheme({ const MessageTheme({
this.replies, this.replies,
this.messageText, this.messageText,
@@ -794,6 +854,40 @@ class MessageTheme {
this.createdAt, this.createdAt,
}); });
/// Text style for message text
final TextStyle? messageText;
/// Text style for message author
final TextStyle? messageAuthor;
/// Text style for message links
final TextStyle? messageLinks;
/// Text style for created at text
final TextStyle? createdAt;
/// Text style for replies
final TextStyle? replies;
/// Color for messageBackgroundColor
final Color? messageBackgroundColor;
/// Color for message border color
final Color? messageBorderColor;
/// Color for reactions
final Color? reactionsBackgroundColor;
/// Colors reaction border
final Color? reactionsBorderColor;
/// Color for reaction mask
final Color? reactionsMaskColor;
/// Theme of the avatar
final AvatarTheme? avatarTheme;
/// Copy with a theme
MessageTheme copyWith({ MessageTheme copyWith({
TextStyle? messageText, TextStyle? messageText,
TextStyle? messageAuthor, TextStyle? messageAuthor,
@@ -823,6 +917,7 @@ class MessageTheme {
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
); );
/// Merge with a theme
MessageTheme merge(MessageTheme? other) { MessageTheme merge(MessageTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -843,14 +938,9 @@ class MessageTheme {
} }
} }
/// Theme for channel preview
class ChannelPreviewTheme { class ChannelPreviewTheme {
final TextStyle? title; /// Constructor for creating [ChannelPreviewTheme]
final TextStyle? subtitle;
final TextStyle? lastMessageAt;
final AvatarTheme? avatarTheme;
final Color? unreadCounterColor;
final double? indicatorIconSize;
const ChannelPreviewTheme({ const ChannelPreviewTheme({
this.title, this.title,
this.subtitle, this.subtitle,
@@ -860,6 +950,25 @@ class ChannelPreviewTheme {
this.indicatorIconSize, this.indicatorIconSize,
}); });
/// Theme for title
final TextStyle? title;
/// Theme for subtitle
final TextStyle? subtitle;
/// Theme of last message at
final TextStyle? lastMessageAt;
/// Avatar theme
final AvatarTheme? avatarTheme;
/// Unread counter color
final Color? unreadCounterColor;
/// Indicator icon size
final double? indicatorIconSize;
/// Copy with theme
ChannelPreviewTheme copyWith({ ChannelPreviewTheme copyWith({
TextStyle? title, TextStyle? title,
TextStyle? subtitle, TextStyle? subtitle,
@@ -877,6 +986,7 @@ class ChannelPreviewTheme {
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
); );
/// Merge with theme
ChannelPreviewTheme merge(ChannelPreviewTheme? other) { ChannelPreviewTheme merge(ChannelPreviewTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -890,12 +1000,9 @@ class ChannelPreviewTheme {
} }
} }
/// Theme for [ChannelHeader]
class ChannelHeaderTheme { class ChannelHeaderTheme {
final TextStyle? title; /// Constructor for creating a [ChannelHeaderTheme]
final TextStyle? subtitle;
final AvatarTheme? avatarTheme;
final Color? color;
const ChannelHeaderTheme({ const ChannelHeaderTheme({
this.title, this.title,
this.subtitle, this.subtitle,
@@ -903,6 +1010,19 @@ class ChannelHeaderTheme {
this.color, this.color,
}); });
/// Theme for title
final TextStyle? title;
/// Theme for subtitle
final TextStyle? subtitle;
/// Theme for avatar
final AvatarTheme? avatarTheme;
/// Color for [ChannelHeaderTheme]
final Color? color;
/// Copy with theme
ChannelHeaderTheme copyWith({ ChannelHeaderTheme copyWith({
TextStyle? title, TextStyle? title,
TextStyle? subtitle, TextStyle? subtitle,
@@ -916,6 +1036,7 @@ class ChannelHeaderTheme {
color: color ?? this.color, color: color ?? this.color,
); );
/// Merge with other [ChannelHeaderTheme]
ChannelHeaderTheme merge(ChannelHeaderTheme? other) { ChannelHeaderTheme merge(ChannelHeaderTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
@@ -929,6 +1050,13 @@ class ChannelHeaderTheme {
/// Theme dedicated to the [ChannelListHeader] /// Theme dedicated to the [ChannelListHeader]
class ChannelListHeaderTheme { class ChannelListHeaderTheme {
/// Returns a new [ChannelListHeaderTheme]
const ChannelListHeaderTheme({
this.title,
this.avatarTheme,
this.color,
});
/// Style of the title text /// Style of the title text
final TextStyle? title; final TextStyle? title;
@@ -938,13 +1066,6 @@ class ChannelListHeaderTheme {
/// Background color of the appbar /// Background color of the appbar
final Color? color; final Color? color;
/// Returns a new [ChannelListHeaderTheme]
const ChannelListHeaderTheme({
this.title,
this.avatarTheme,
this.color,
});
/// Returns a new [ChannelListHeaderTheme] replacing some of its properties /// Returns a new [ChannelListHeaderTheme] replacing some of its properties
ChannelListHeaderTheme copyWith({ ChannelListHeaderTheme copyWith({
TextStyle? title, TextStyle? title,
@@ -970,6 +1091,22 @@ class ChannelListHeaderTheme {
/// Defines the theme dedicated to the [MessageInput] widget /// Defines the theme dedicated to the [MessageInput] widget
class MessageInputTheme { class MessageInputTheme {
/// Returns a new [MessageInputTheme]
const MessageInputTheme({
this.sendAnimationDuration,
this.actionButtonColor,
this.sendButtonColor,
this.actionButtonIdleColor,
this.sendButtonIdleColor,
this.inputBackground,
this.inputTextStyle,
this.inputDecoration,
this.activeBorderGradient,
this.idleBorderGradient,
this.borderRadius,
this.expandButtonColor,
});
/// Duration of the [MessageInput] send button animation /// Duration of the [MessageInput] send button animation
final Duration? sendAnimationDuration; final Duration? sendAnimationDuration;
@@ -1006,22 +1143,6 @@ class MessageInputTheme {
/// Border radius of [MessageInput] /// Border radius of [MessageInput]
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// Returns a new [MessageInputTheme]
const MessageInputTheme({
this.sendAnimationDuration,
this.actionButtonColor,
this.sendButtonColor,
this.actionButtonIdleColor,
this.sendButtonIdleColor,
this.inputBackground,
this.inputTextStyle,
this.inputDecoration,
this.activeBorderGradient,
this.idleBorderGradient,
this.borderRadius,
this.expandButtonColor,
});
/// Returns a new [MessageInputTheme] replacing some of its properties /// Returns a new [MessageInputTheme] replacing some of its properties
MessageInputTheme copyWith({ MessageInputTheme copyWith({
Duration? sendAnimationDuration, Duration? sendAnimationDuration,
@@ -1075,13 +1196,9 @@ class MessageInputTheme {
} }
} }
/// Effect store
class Effect { class Effect {
final double? sigmaX; /// Constructor for creating [Effect]
final double? sigmaY;
final Color? color;
final double? alpha;
final double? blur;
const Effect({ const Effect({
this.sigmaX, this.sigmaX,
this.sigmaY, this.sigmaY,
@@ -1090,6 +1207,22 @@ class Effect {
this.blur, this.blur,
}); });
///
final double? sigmaX;
///
final double? sigmaY;
///
final Color? color;
///
final double? alpha;
///
final double? blur;
/// Copy with new effect
Effect copyWith({ Effect copyWith({
double? sigmaX, double? sigmaX,
double? sigmaY, double? sigmaY,
@@ -1,19 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Neumorphic button
class StreamNeumorphicButton extends StatelessWidget { class StreamNeumorphicButton extends StatelessWidget {
final Widget child; /// Constructor for creating [StreamNeumorphicButton]
final Color backgroundColor;
const StreamNeumorphicButton({ const StreamNeumorphicButton({
Key? key, Key? key,
required this.child, required this.child,
this.backgroundColor = Colors.white, this.backgroundColor = Colors.white,
}) : super(key: key); }) : super(key: key);
/// Child contained in the button
final Widget child;
/// Background color of button
final Color backgroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Container(
return Container( margin: const EdgeInsets.all(8),
margin: EdgeInsets.all(8.0),
height: 40, height: 40,
width: 40, width: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -22,19 +26,15 @@ class StreamNeumorphicButton extends StatelessWidget {
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.shade700, color: Colors.grey.shade700,
offset: Offset(0, 1.0), offset: const Offset(0, 1),
blurRadius: 0.5, blurRadius: 0.5,
spreadRadius: 0,
), ),
BoxShadow( const BoxShadow(
color: Colors.white, color: Colors.white,
offset: Offset.zero,
blurRadius: 0.5, blurRadius: 0.5,
spreadRadius: 0,
), ),
], ],
), ),
child: child, child: child,
); );
} }
}
File diff suppressed because it is too large Load Diff
@@ -1,34 +1,45 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_chat_theme.dart'; /// Widget to make a swipeable tile
///
class Swipeable extends StatefulWidget { class Swipeable extends StatefulWidget {
final Widget child; /// Constructor for creating a [Swipeable] widget
final Widget backgroundIcon;
final VoidCallback? onSwipeStart;
final VoidCallback? onSwipeCancel;
final VoidCallback? onSwipeEnd;
final double threshold;
///
const Swipeable({ const Swipeable({
Key? key,
required this.child, required this.child,
required this.backgroundIcon, required this.backgroundIcon,
this.onSwipeStart, this.onSwipeStart,
this.onSwipeCancel, this.onSwipeCancel,
this.onSwipeEnd, this.onSwipeEnd,
this.threshold = 82.0, this.threshold = 82.0,
}); }) : super(key: key);
/// Child to make swipeable
final Widget child;
/// Background icon after swipe
final Widget backgroundIcon;
/// Callback when swipe starts
final VoidCallback? onSwipeStart;
/// Callback when swipe is cancelled
final VoidCallback? onSwipeCancel;
/// Callback when swipe ends
final VoidCallback? onSwipeEnd;
/// Threshold for swipe
final double threshold;
@override @override
State<StatefulWidget> createState() => _SwipeableState(); State<StatefulWidget> createState() => _SwipeableState();
} }
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin { class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
double _dragExtent = 0.0; double _dragExtent = 0;
late AnimationController _moveController; late AnimationController _moveController;
late AnimationController _iconMoveController; late AnimationController _iconMoveController;
late Animation<Offset> _moveAnimation; late Animation<Offset> _moveAnimation;
@@ -45,15 +56,15 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
AnimationController(duration: _animationDuration, vsync: this); AnimationController(duration: _animationDuration, vsync: this);
_iconMoveController = _iconMoveController =
AnimationController(duration: _animationDuration, vsync: this); AnimationController(duration: _animationDuration, vsync: this);
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0)) _moveAnimation = Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))
.animate(_moveController); .animate(_moveController);
_iconTransitionAnimation = _iconTransitionAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0)) Tween<Offset>(begin: const Offset(-0.1, 0), end: const Offset(0.4, 0))
.animate(_moveController); .animate(_moveController);
_iconFadeAnimation = _iconFadeAnimation =
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController); Tween<double>(begin: 0.7, end: 1).animate(_iconMoveController);
final controllerValue = 0.0; const controllerValue = 0.0;
_moveController.animateTo(controllerValue); _moveController.animateTo(controllerValue);
_iconMoveController.animateTo(controllerValue); _iconMoveController.animateTo(controllerValue);
} }
@@ -77,19 +88,19 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
if (_dragExtent.isNegative) return; if (_dragExtent.isNegative) return;
var movePastThresholdPixels = widget.threshold; final movePastThresholdPixels = widget.threshold;
var newPos = _dragExtent.abs() / context.size!.width; var newPos = _dragExtent.abs() / context.size!.width;
if (_dragExtent.abs() > movePastThresholdPixels) { if (_dragExtent.abs() > movePastThresholdPixels) {
// how many "thresholds" past the threshold we are. 1 = the threshold 2 // how many "thresholds" past the threshold we are. 1 = the threshold 2
// = two thresholds. // = two thresholds.
var n = _dragExtent.abs() / movePastThresholdPixels; final n = _dragExtent.abs() / movePastThresholdPixels;
// Take the number of thresholds past the threshold, and reduce this // Take the number of thresholds past the threshold, and reduce this
// number // number
var reducedThreshold = math.pow(n, 0.3); final reducedThreshold = math.pow(n, 0.3);
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold; final adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
newPos = adjustedPixelPos / context.size!.width; newPos = adjustedPixelPos / context.size!.width;
if (_dragExtent > 0 && !_pastThreshold) { if (_dragExtent > 0 && !_pastThreshold) {
@@ -111,8 +122,8 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
_moveController.animateTo(0.0, duration: _animationDuration); _moveController.animateTo(0, duration: _animationDuration);
_iconMoveController.animateTo(0.0, duration: _animationDuration); _iconMoveController.animateTo(0, duration: _animationDuration);
_dragExtent = 0.0; _dragExtent = 0.0;
if (_pastThreshold && widget.onSwipeEnd != null) { if (_pastThreshold && widget.onSwipeEnd != null) {
widget.onSwipeEnd!(); widget.onSwipeEnd!();
@@ -120,8 +131,7 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector(
onHorizontalDragStart: _handleDragStart, onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd, onHorizontalDragEnd: _handleDragEnd,
@@ -160,4 +170,3 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
), ),
); );
} }
}
@@ -3,18 +3,20 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference /// It shows a date divider depending on the date difference
class SystemMessage extends StatelessWidget { class SystemMessage extends StatelessWidget {
/// This message /// Constructor for creating a [SystemMessage]
final Message message;
/// The function called when tapping on the message when the message is not failed
final void Function(Message)? onMessageTap;
const SystemMessage({ const SystemMessage({
Key? key, Key? key,
required this.message, required this.message,
this.onMessageTap, this.onMessageTap,
}) : super(key: key); }) : super(key: key);
/// This message
final Message message;
// ignore: lines_longer_than_80_chars
/// The function called when tapping on the message when the message is not failed
final void Function(Message)? onMessageTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -3,9 +3,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'back_button.dart';
import 'channel_name.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png)
/// ///
@@ -46,16 +43,34 @@ import 'channel_name.dart';
/// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget. /// However you can also use it as a normal widget.
/// ///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel. /// Make sure to have a [StreamChannel] ancestor in order to provide the
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates. /// information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel
/// information as soon as it updates.
/// ///
/// By default the widget shows a backButton that calls [Navigator.pop]. /// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just override the behaviour /// You can disable this button using the [showBackButton] property of just
/// override the behaviour
/// with [onBackPressed]. /// with [onBackPressed].
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// Instantiate a new ThreadHeader
const ThreadHeader({
Key? key,
required this.parent,
this.showBackButton = true,
this.onBackPressed,
this.title,
this.subtitle,
this.leading,
this.actions,
this.onTitleTap,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// True if this header shows the leading back button /// True if this header shows the leading back button
final bool showBackButton; final bool showBackButton;
@@ -81,23 +96,8 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// AppBar actions /// AppBar actions
final List<Widget>? actions; final List<Widget>? actions;
/// Instantiate a new ThreadHeader
ThreadHeader({
Key? key,
required this.parent,
this.showBackButton = true,
this.onBackPressed,
this.title,
this.subtitle,
this.leading,
this.actions,
this.onTitleTap,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => AppBar(
return AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
@@ -108,17 +108,16 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
onPressed: onBackPressed, onPressed: onBackPressed,
showUnreads: true, showUnreads: true,
) )
: SizedBox()), : const SizedBox()),
backgroundColor: backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
centerTitle: true, centerTitle: true,
actions: actions, actions: actions,
title: InkWell( title: InkWell(
onTap: onTitleTap, onTap: onTitleTap,
child: Container( child: SizedBox(
height: preferredSize.height, height: preferredSize.height,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
title ?? title ??
@@ -129,11 +128,10 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
.channelHeaderTheme .channelHeaderTheme
.title, .title,
), ),
SizedBox(height: 2), const SizedBox(height: 2),
subtitle ?? subtitle ??
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
@@ -158,7 +156,6 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
); );
}
@override @override
final Size preferredSize; final Size preferredSize;
@@ -26,6 +26,7 @@ class TypingIndicator extends StatelessWidget {
/// The padding of this widget /// The padding of this widget
final EdgeInsets padding; final EdgeInsets padding;
/// Alignment of the typing indicator
final Alignment alignment; final Alignment alignment;
@override @override
@@ -35,14 +36,13 @@ class TypingIndicator extends StatelessWidget {
return StreamBuilder<List<User>>( return StreamBuilder<List<User>>(
initialData: channelState.typingEvents, initialData: channelState.typingEvents,
stream: channelState.typingEventsStream, stream: channelState.typingEventsStream,
builder: (context, snapshot) { builder: (context, snapshot) => AnimatedSwitcher(
return AnimatedSwitcher( duration: const Duration(milliseconds: 300),
duration: Duration(milliseconds: 300),
child: snapshot.data?.isNotEmpty == true child: snapshot.data?.isNotEmpty == true
? Padding( ? Padding(
padding: padding, padding: padding,
child: Align( child: Align(
key: Key('typings'), key: const Key('typings'),
alignment: alignment, alignment: alignment,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -53,6 +53,7 @@ class TypingIndicator extends StatelessWidget {
height: 4, height: 4,
), ),
Text( Text(
// ignore: lines_longer_than_80_chars
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing', ' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
maxLines: 1, maxLines: 1,
style: style, style: style,
@@ -62,14 +63,13 @@ class TypingIndicator extends StatelessWidget {
), ),
) )
: Align( : Align(
key: Key('alternative'), key: const Key('alternative'),
alignment: alignment, alignment: alignment,
child: Container( child: Container(
child: alternativeWidget ?? Offstage(), child: alternativeWidget ?? const Offstage(),
),
), ),
), ),
); );
},
);
} }
} }
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget for showing an unread indicator
class UnreadIndicator extends StatelessWidget { class UnreadIndicator extends StatelessWidget {
/// Constructor for creating an [UnreadIndicator]
const UnreadIndicator({ const UnreadIndicator({
Key? key, Key? key,
this.cid, this.cid,
@@ -24,7 +26,7 @@ class UnreadIndicator extends StatelessWidget {
: client.state.totalUnreadCount, : client.state.totalUnreadCount,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) { if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox(); return const SizedBox();
} }
return Material( return Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -33,15 +35,15 @@ class UnreadIndicator extends StatelessWidget {
.unreadCounterColor, .unreadCounterColor,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 5.0, left: 5,
right: 5.0, right: 5,
top: 2, top: 2,
bottom: 1, bottom: 1,
), ),
child: Center( child: Center(
child: Text( child: Text(
'${snapshot.data! > 99 ? '99+' : snapshot.data}', '${snapshot.data! > 99 ? '99+' : snapshot.data}',
style: TextStyle( style: const TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.white, color: Colors.white,
), ),
@@ -1,16 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_chat_theme.dart'; /// Widget for showing upload progress
class UploadProgressIndicator extends StatelessWidget { class UploadProgressIndicator extends StatelessWidget {
final int uploaded; /// Constructor for creating an [UploadProgressIndicator]
final int total; const UploadProgressIndicator({
final Color progressIndicatorColor;
final EdgeInsetsGeometry padding;
final bool showBackground;
final TextStyle? textStyle;
UploadProgressIndicator({
Key? key, Key? key,
required this.uploaded, required this.uploaded,
required this.total, required this.total,
@@ -25,6 +19,24 @@ class UploadProgressIndicator extends StatelessWidget {
this.textStyle, this.textStyle,
}) : super(key: key); }) : super(key: key);
/// Bytes uploaded
final int uploaded;
/// Total bytes
final int total;
/// Color of progress indicator
final Color progressIndicatorColor;
/// Padding for widget
final EdgeInsetsGeometry padding;
/// Flag for showing background
final bool showBackground;
/// [TextStyle] to be applied to text
final TextStyle? textStyle;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -42,7 +54,7 @@ class UploadProgressIndicator extends StatelessWidget {
valueColor: AlwaysStoppedAnimation(progressIndicatorColor), valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
), ),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${_percentage.toInt()}%', '${_percentage.toInt()}%',
style: textStyle ?? style: textStyle ??
@@ -3,23 +3,30 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget to display URL attachment
class UrlAttachment extends StatelessWidget { class UrlAttachment extends StatelessWidget {
final Attachment urlAttachment; /// Constructor for creating a [UrlAttachment]
final String hostDisplayName; const UrlAttachment({
final EdgeInsets textPadding; Key? key,
UrlAttachment({
required this.urlAttachment, required this.urlAttachment,
required this.hostDisplayName, required this.hostDisplayName,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16,
vertical: 8.0, vertical: 8,
), ),
}); }) : super(key: key);
/// Attachment to be displayed
final Attachment urlAttachment;
/// Host name
final String hostDisplayName;
/// Padding for text
final EdgeInsets textPadding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector(
onTap: () { onTap: () {
launchURL( launchURL(
context, context,
@@ -32,9 +39,9 @@ class UrlAttachment extends StatelessWidget {
if (urlAttachment.imageUrl != null) if (urlAttachment.imageUrl != null)
Container( Container(
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
margin: EdgeInsets.symmetric(horizontal: 8.0), margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8),
), ),
child: Stack( child: Stack(
children: [ children: [
@@ -44,20 +51,21 @@ class UrlAttachment extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
Positioned( Positioned(
left: 0.0, left: 0,
bottom: -1, bottom: -1,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
), ),
color: StreamChatTheme.of(context).colorTheme.blueAlice, color:
StreamChatTheme.of(context).colorTheme.blueAlice,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 8.0, top: 8,
left: 8.0, left: 8,
right: 8.0, right: 8,
), ),
child: Text( child: Text(
hostDisplayName, hostDisplayName,
@@ -106,4 +114,3 @@ class UrlAttachment extends StatelessWidget {
), ),
); );
} }
}
@@ -1,10 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart'; /// Widget that displays a user avatar
class UserAvatar extends StatelessWidget { class UserAvatar extends StatelessWidget {
/// Constructor to create a [UserAvatar]
const UserAvatar({ const UserAvatar({
Key? key, Key? key,
required this.user, required this.user,
@@ -20,16 +21,37 @@ class UserAvatar extends StatelessWidget {
this.selectionThickness = 4, this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
/// User whose avatar is to displayed
final User user; final User user;
/// Alignment of the online indicator
final Alignment onlineIndicatorAlignment; final Alignment onlineIndicatorAlignment;
/// Size of the avatar
final BoxConstraints? constraints; final BoxConstraints? constraints;
/// [BorderRadius] of the image
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// Size of the online indicator
final BoxConstraints? onlineIndicatorConstraints; final BoxConstraints? onlineIndicatorConstraints;
/// Callback when avatar is tapped
final void Function(User)? onTap; final void Function(User)? onTap;
/// Callback when avatar is long pressed
final void Function(User)? onLongPress; final void Function(User)? onLongPress;
/// Flag for showing online status
final bool showOnlineStatus; final bool showOnlineStatus;
/// Flag for if avatar is selected
final bool selected; final bool selected;
/// Color of selection
final Color? selectionColor; final Color? selectionColor;
/// Selection thickness around the avatar
final double selectionThickness; final double selectionThickness;
@override @override
@@ -42,7 +64,6 @@ class UserAvatar extends StatelessWidget {
Widget avatar = FittedBox( Widget avatar = FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: ClipRRect( child: ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius ?? borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
child: Container( child: Container(
@@ -54,10 +75,10 @@ class UserAvatar extends StatelessWidget {
child: hasImage child: hasImage
? CachedNetworkImage( ? CachedNetworkImage(
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
// ignore: cast_nullable_to_non_nullable
imageUrl: user.extraData['image'] as String, imageUrl: user.extraData['image'] as String,
errorWidget: (_, __, ___) { errorWidget: (_, __, ___) =>
return streamChatTheme.defaultUserImage(context, user); streamChatTheme.defaultUserImage(context, user),
},
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: streamChatTheme.defaultUserImage(context, user), : streamChatTheme.defaultUserImage(context, user),
@@ -97,14 +118,14 @@ class UserAvatar extends StatelessWidget {
type: MaterialType.circle, type: MaterialType.circle,
color: streamChatTheme.colorTheme.white, color: streamChatTheme.colorTheme.white,
child: Container( child: Container(
margin: const EdgeInsets.all(2.0), margin: const EdgeInsets.all(2),
constraints: onlineIndicatorConstraints ?? constraints: onlineIndicatorConstraints ??
BoxConstraints.tightFor( const BoxConstraints.tightFor(
width: 8, width: 8,
height: 8, height: 8,
), ),
child: Material( child: Material(
shape: CircleBorder(), shape: const CircleBorder(),
color: streamChatTheme.colorTheme.accentGreen, color: streamChatTheme.colorTheme.accentGreen,
), ),
), ),
@@ -5,16 +5,17 @@ import 'package:stream_chat_flutter/src/user_list_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'stream_chat_theme.dart';
/// ///
/// It shows the current [User] preview. /// It shows the current [User] preview.
/// ///
/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates. /// The widget uses a [StreamBuilder] to render the user information
/// image as soon as it updates.
/// ///
/// Usually you don't use this widget as it's the default user preview used by [UserListView]. /// Usually you don't use this widget as it's the default user preview used
/// by [UserListView].
/// ///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class UserItem extends StatelessWidget { class UserItem extends StatelessWidget {
/// Instantiate a new UserItem /// Instantiate a new UserItem
@@ -47,8 +48,7 @@ class UserItem extends StatelessWidget {
final bool showLastOnline; final bool showLastOnline;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => ListTile(
return ListTile(
onTap: () { onTap: () {
if (onTap != null) { if (onTap != null) {
onTap!(user); onTap!(user);
@@ -61,13 +61,12 @@ class UserItem extends StatelessWidget {
}, },
leading: UserAvatar( leading: UserAvatar(
user: user, user: user,
showOnlineStatus: true,
onTap: (user) { onTap: (user) {
if (onImageTap != null) { if (onImageTap != null) {
onImageTap!(user); onImageTap!(user);
} }
}, },
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
width: 40, width: 40,
), ),
@@ -83,15 +82,13 @@ class UserItem extends StatelessWidget {
), ),
subtitle: showLastOnline ? _buildLastActive(context) : null, subtitle: showLastOnline ? _buildLastActive(context) : null,
); );
}
Widget _buildLastActive(context) { Widget _buildLastActive(context) => Text(
return Text(
user.online == true user.online == true
? 'Online' ? 'Online'
: 'Last online ${Jiffy(user.lastActive).fromNow()}', : 'Last online ${Jiffy(user.lastActive).fromNow()}',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith( style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
); );
} }
}
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'user_item.dart';
/// Callback called when tapping on a user /// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget?); typedef UserTapCallback = void Function(User, Widget?);
@@ -36,10 +34,13 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
/// ``` /// ```
/// ///
/// ///
/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users. /// Make sure to have a [UsersBloc] ancestor in order to provide the
/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels. /// information about the users.
/// The widget uses a [ListView.separated], [GridView.builder] to render the
/// list, grid of channels.
/// ///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// The widget components render the ui based on the first ancestor of
/// type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class UserListView extends StatefulWidget { class UserListView extends StatefulWidget {
/// Instantiate a new UserListView /// Instantiate a new UserListView
@@ -81,8 +82,10 @@ class UserListView extends StatefulWidget {
final Map<String, dynamic>? options; final Map<String, dynamic>? options;
/// The sorting used for the channels matching the filters. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// 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. /// Direction can be ascending or descending.
final List<SortOption>? sort; final List<SortOption>? sort;
@@ -115,7 +118,8 @@ class UserListView extends StatefulWidget {
/// Set it to false to disable the pull-to-refresh widget /// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh; final bool pullToRefresh;
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers] /// Sets a blue trailing checkMark in [ListUserItem] for all the
/// [selectedUsers]
final Set<User>? selectedUsers; final Set<User>? selectedUsers;
/// Set it to true to group users by their first character /// Set it to true to group users by their first character
@@ -151,37 +155,27 @@ class _UserListViewState extends State<UserListView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var child = UserListCore( final child = UserListCore(
errorBuilder: widget.errorBuilder as Widget Function(Object)? ?? errorBuilder: widget.errorBuilder as Widget Function(Object)? ??
(err) { (err) => _buildError(err as Error),
return _buildError(err as Error); emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(),
},
emptyBuilder: widget.emptyBuilder ??
(context) {
return _buildEmpty();
},
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) { (context) => LayoutBuilder(
return LayoutBuilder( builder: (context, viewportConstraints) =>
builder: (context, viewportConstraints) { SingleChildScrollView(
return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(),
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Center( child: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
); ),
}, ),
); listBuilder:
}, widget.listBuilder ?? (context, list) => _buildListView(list),
listBuilder: widget.listBuilder ??
(context, list) {
return _buildListView(list);
},
pagination: widget.pagination, pagination: widget.pagination,
options: widget.options, options: widget.options,
sort: widget.sort, sort: widget.sort,
@@ -204,7 +198,7 @@ class _UserListViewState extends State<UserListView>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Widget _buildError(Error error) { Widget _buildError(Error error) {
print((error).stackTrace); print(error.stackTrace);
var message = error.toString(); var message = error.toString();
if (error is DioError) { if (error is DioError) {
@@ -220,12 +214,12 @@ class _UserListViewState extends State<UserListView>
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text.rich( Text.rich(
TextSpan( const TextSpan(
children: [ children: [
WidgetSpan( WidgetSpan(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: EdgeInsets.only(
right: 2.0, right: 2,
), ),
child: Icon(Icons.error_outline), child: Icon(Icons.error_outline),
), ),
@@ -237,43 +231,39 @@ class _UserListViewState extends State<UserListView>
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 16.0, top: 16,
), ),
child: Text(message), child: Text(message),
), ),
TextButton( TextButton(
onPressed: () => _userListController.loadData!(), onPressed: () => _userListController.loadData!(),
child: Text('Retry'), child: const Text('Retry'),
), ),
], ],
), ),
); );
} }
Widget _buildEmpty() { Widget _buildEmpty() => LayoutBuilder(
return LayoutBuilder( builder: (context, viewportConstraints) => SingleChildScrollView(
builder: (context, viewportConstraints) { physics: const AlwaysScrollableScrollPhysics(),
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Center( child: const Center(
child: Text('There are no users currently'), child: Text('There are no users currently'),
), ),
), ),
),
); );
},
);
}
Widget _buildListView( Widget _buildListView(
List<ListItem> items, List<ListItem> items,
) { ) {
final child = _isListView final child = _isListView
? ListView.separated( ? ListView.separated(
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length, itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) { separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) { if (widget.separatorBuilder != null) {
@@ -281,19 +271,17 @@ class _UserListViewState extends State<UserListView>
} }
return _separatorBuilder(context, index); return _separatorBuilder(context, index);
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) =>
return _listItemBuilder(context, index, items); _listItemBuilder(context, index, items),
},
) )
: GridView.builder( : GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount, crossAxisCount: widget.crossAxisCount,
), ),
itemCount: items.isNotEmpty ? items.length + 1 : items.length, itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) =>
return _gridItemBuilder(context, index, items); _gridItemBuilder(context, index, items),
},
); );
return LazyLoadScrollView( return LazyLoadScrollView(
@@ -307,13 +295,11 @@ class _UserListViewState extends State<UserListView>
if (i < items.length) { if (i < items.length) {
final item = items[i]; final item = items[i];
return item.when( return item.when(
headerItem: (header) { headerItem: (header) => Container(
return Container(
key: ValueKey<String>('HEADER-$header'), key: ValueKey<String>('HEADER-$header'),
color: color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text( child: Text(
header, header,
style: TextStyle( style: TextStyle(
@@ -323,8 +309,7 @@ class _UserListViewState extends State<UserListView>
), ),
), ),
), ),
); ),
},
userItem: (user) { userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false; final selected = widget.selectedUsers?.contains(user) ?? false;
return Container( return Container(
@@ -351,7 +336,7 @@ class _UserListViewState extends State<UserListView>
if (i < items.length) { if (i < items.length) {
final item = items[i]; final item = items[i];
return item.when( return item.when(
headerItem: (_) => Offstage(), headerItem: (_) => const Offstage(),
userItem: (user) { userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false; final selected = widget.selectedUsers?.contains(user) ?? false;
return Container( return Container(
@@ -360,17 +345,17 @@ class _UserListViewState extends State<UserListView>
? widget.userItemBuilder!(context, user, selected) ? widget.userItemBuilder!(context, user, selected)
: Column( : Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
UserAvatar( UserAvatar(
user: user, user: user,
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
selected: selected, selected: selected,
constraints: BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 64, height: 64,
width: 64, width: 64,
), ),
onlineIndicatorConstraints: BoxConstraints.tightFor( onlineIndicatorConstraints:
const BoxConstraints.tightFor(
height: 12, height: 12,
width: 12, width: 12,
), ),
@@ -378,7 +363,7 @@ class _UserListViewState extends State<UserListView>
widget.onUserTap!(user, widget.userWidget), widget.onUserTap!(user, widget.userWidget),
onLongPress: widget.onUserLongPress, onLongPress: widget.onUserLongPress,
), ),
SizedBox(height: 4), const SizedBox(height: 4),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text( child: Text(
@@ -386,7 +371,7 @@ class _UserListViewState extends State<UserListView>
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 12,
), ),
@@ -402,8 +387,8 @@ class _UserListViewState extends State<UserListView>
} }
} }
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) =>
return StreamBuilder<bool>( StreamBuilder<bool>(
stream: usersProvider.queryUsersLoading, stream: usersProvider.queryUsersLoading,
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -413,8 +398,8 @@ class _UserListViewState extends State<UserListView>
.colorTheme .colorTheme
.accentRed .accentRed
.withOpacity(.2), .withOpacity(.2),
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: Text('Error loading users'), child: Text('Error loading users'),
), ),
@@ -423,18 +408,17 @@ class _UserListViewState extends State<UserListView>
} }
return Container( return Container(
height: 100, height: 100,
padding: EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Center( child: Center(
child: snapshot.data! ? CircularProgressIndicator() : Container(), child: snapshot.data!
? const CircularProgressIndicator()
: Container(),
), ),
); );
}); });
}
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) => Container(
return Container(
height: 1, height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
); );
} }
}
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Displays a list of users who reacted
class UserReactionDisplay extends StatelessWidget { class UserReactionDisplay extends StatelessWidget {
/// Constructor for creating a [UserReactionDisplay]
const UserReactionDisplay({ const UserReactionDisplay({
Key? key, Key? key,
required this.reactionToEmoji, required this.reactionToEmoji,
@@ -10,24 +12,27 @@ class UserReactionDisplay extends StatelessWidget {
this.size = 30, this.size = 30,
}) : super(key: key); }) : super(key: key);
/// Reaction map
final Map<String, String> reactionToEmoji; final Map<String, String> reactionToEmoji;
/// Message which is reacted to
final Message message; final Message message;
/// Size of Icon
final double size; final double size;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Container(
return Container(
color: Colors.black87, color: Colors.black87,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) { children: reactionToEmoji.keys.map((reactionType) {
var firstUserReaction = message.latestReactions! final firstUserReaction = message.latestReactions!
.firstWhere((element) => element.type == reactionType, .firstWhere((element) => element.type == reactionType,
orElse: () { //ignore: unnecessary_parenthesis
return null; orElse: (() => null) as Reaction Function()?);
} as Reaction Function()?);
if (firstUserReaction.user == null) { if (firstUserReaction.user == null) {
return IconButton( return IconButton(
@@ -53,4 +58,3 @@ class UserReactionDisplay extends StatelessWidget {
), ),
); );
} }
}
+41 -43
View File
@@ -3,23 +3,23 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../stream_chat_flutter.dart'; /// Launch URL
import 'stream_svg_icon.dart';
Future<void> launchURL(BuildContext context, String? url) async { Future<void> launchURL(BuildContext context, String? url) async {
if (url != null && await canLaunch(url)) { if (url != null && await canLaunch(url)) {
await launch(url); await launch(url);
} else { } else {
// ignore: deprecated_member_use // ignore: deprecated_member_use
Scaffold.of(context).showSnackBar( Scaffold.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text('Cannot launch the url'), content: Text('Cannot launch the url'),
), ),
); );
} }
} }
/// Shows confirmation dialog
Future<bool?> showConfirmationDialog( Future<bool?> showConfirmationDialog(
BuildContext context, { BuildContext context, {
required String title, required String title,
@@ -27,14 +27,14 @@ Future<bool?> showConfirmationDialog(
Widget? icon, Widget? icon,
String? question, String? question,
String? cancelText, String? cancelText,
}) { }) =>
return showModalBottomSheet( showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
context: context, context: context,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16),
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
)), )),
builder: (context) { builder: (context) {
final effect = StreamChatTheme.of(context).colorTheme.borderTop; final effect = StreamChatTheme.of(context).colorTheme.borderTop;
@@ -42,20 +42,20 @@ Future<bool?> showConfirmationDialog(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(height: 26.0), const SizedBox(height: 26),
if (icon != null) icon, if (icon != null) icon,
SizedBox(height: 26.0), const SizedBox(height: 26),
Text( Text(
title, title,
style: StreamChatTheme.of(context).textTheme.headlineBold, style: StreamChatTheme.of(context).textTheme.headlineBold,
), ),
SizedBox(height: 7.0), const SizedBox(height: 7),
if (question != null) if (question != null)
Text( Text(
question, question,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 36.0), const SizedBox(height: 36),
Container( Container(
color: effect.color!.withOpacity(effect.alpha ?? 1), color: effect.color!.withOpacity(effect.alpha ?? 1),
height: 1, height: 1,
@@ -110,8 +110,8 @@ Future<bool?> showConfirmationDialog(
), ),
); );
}); });
}
/// Shows info dialog
Future<bool?> showInfoDialog( Future<bool?> showInfoDialog(
BuildContext context, { BuildContext context, {
required String title, required String title,
@@ -119,44 +119,43 @@ Future<bool?> showInfoDialog(
Widget? icon, Widget? icon,
String? details, String? details,
StreamChatThemeData? theme, StreamChatThemeData? theme,
}) { }) =>
return showModalBottomSheet( showModalBottomSheet(
backgroundColor: backgroundColor: theme?.colorTheme.white ??
theme?.colorTheme.white ?? StreamChatTheme.of(context).colorTheme.white, StreamChatTheme.of(context).colorTheme.white,
context: context, context: context,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16),
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
)), )),
builder: (context) { builder: (context) => SafeArea(
return SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( const SizedBox(
height: 26.0, height: 26,
), ),
if (icon != null) icon, if (icon != null) icon,
SizedBox( const SizedBox(
height: 26.0, height: 26,
), ),
Text( Text(
title, title,
style: theme?.textTheme.headlineBold ?? style: theme?.textTheme.headlineBold ??
StreamChatTheme.of(context).textTheme.headlineBold, StreamChatTheme.of(context).textTheme.headlineBold,
), ),
SizedBox( const SizedBox(
height: 7.0, height: 7,
), ),
if (details != null) Text(details), if (details != null) Text(details),
SizedBox( const SizedBox(
height: 36.0, height: 36,
), ),
Container( Container(
color: theme?.colorTheme.black.withOpacity(.08) ?? color: theme?.colorTheme.black.withOpacity(.08) ??
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
height: 1.0, height: 1,
), ),
Center( Center(
child: TextButton( child: TextButton(
@@ -175,10 +174,8 @@ Future<bool?> showInfoDialog(
), ),
], ],
), ),
),
); );
},
);
}
/// Get random png with initials /// Get random png with initials
String getRandomPicUrl(User user) => String getRandomPicUrl(User user) =>
@@ -236,7 +233,7 @@ String fileSize(dynamic size, [int round = 2]) {
* the optional parameter [round] specifies the number * the optional parameter [round] specifies the number
* of digits after comma/point (default is 2) * of digits after comma/point (default is 2)
*/ */
final divider = 1024; const divider = 1024;
int _size; int _size;
try { try {
_size = int.parse(size.toString()); _size = int.parse(size.toString());
@@ -274,21 +271,21 @@ String fileSize(dynamic size, [int round = 2]) {
if (_size < divider * divider * divider * divider * divider && if (_size < divider * divider * divider * divider * divider &&
_size % divider == 0) { _size % divider == 0) {
num r = _size / divider / divider / divider / divider; final num r = _size / divider / divider / divider / divider;
return '${r.toStringAsFixed(0)} TB'; return '${r.toStringAsFixed(0)} TB';
} }
if (_size < divider * divider * divider * divider * divider) { if (_size < divider * divider * divider * divider * divider) {
num r = _size / divider / divider / divider / divider; final num r = _size / divider / divider / divider / divider;
return '${r.toStringAsFixed(round)} TB'; return '${r.toStringAsFixed(round)} TB';
} }
if (_size < divider * divider * divider * divider * divider * divider && if (_size < divider * divider * divider * divider * divider * divider &&
_size % divider == 0) { _size % divider == 0) {
num r = _size / divider / divider / divider / divider / divider; final num r = _size / divider / divider / divider / divider / divider;
return '${r.toStringAsFixed(0)} PB'; return '${r.toStringAsFixed(0)} PB';
} else { } else {
num r = _size / divider / divider / divider / divider / divider; final num r = _size / divider / divider / divider / divider / divider;
return '${r.toStringAsFixed(round)} PB'; return '${r.toStringAsFixed(round)} PB';
} }
} }
@@ -335,14 +332,16 @@ StreamSvgIcon getFileTypeImage(String? type) {
} }
} }
/// Wraps attachment widget with custom shape
Widget wrapAttachmentWidget( Widget wrapAttachmentWidget(
BuildContext context, BuildContext context,
Widget attachmentWidget, Widget attachmentWidget,
ShapeBorder attachmentShape, ShapeBorder attachmentShape,
// ignore: avoid_positional_boolean_parameters
bool reverse, bool reverse,
BorderRadius borderRadius, BorderRadius borderRadius,
) { ) =>
return ClipRRect( ClipRRect(
borderRadius: borderRadius, borderRadius: borderRadius,
child: Material( child: Material(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
@@ -355,4 +354,3 @@ Widget wrapAttachmentWidget(
), ),
), ),
); );
}
@@ -5,12 +5,14 @@ import 'package:synchronized/synchronized.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
///
class IVideoService { class IVideoService {
IVideoService._();
/// Singleton instance of [IVideoService]
static final IVideoService instance = IVideoService._(); static final IVideoService instance = IVideoService._();
final _lock = Lock(); final _lock = Lock();
IVideoService._();
/// compress video from [path] /// compress video from [path]
/// compress video from [path] return [Future<MediaInfo>] /// compress video from [path] return [Future<MediaInfo>]
/// ///
@@ -26,18 +28,20 @@ class IVideoService {
/// ); /// );
/// debugPrint(info.toJson()); /// debugPrint(info.toJson());
/// ``` /// ```
Future<MediaInfo?> compressVideo(String? path) async { Future<MediaInfo?> compressVideo(String? path) async => _lock.synchronized(
return _lock.synchronized(() { () => VideoCompress.compressVideo(
return VideoCompress.compressVideo(
path!, path!,
),
); );
});
}
/// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...). /// Generates a thumbnail image data in memory as UInt8List,
/// The video can be a local video file, or an URL repreents iOS or Android native supported video format. /// it can be easily used by Image.memory(...).
/// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video. /// The video can be a local video file, or an URL repreents iOS or
/// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format. /// Android native supported video format.
/// Speicify the maximum height or width for the thumbnail or 0 for
/// same resolution as the original video.
/// The lower quality value creates lower quality of the thumbnail image,
/// but it gets ignored for PNG format.
Future<Uint8List?> generateVideoThumbnail({ Future<Uint8List?> generateVideoThumbnail({
required String video, required String video,
ImageFormat imageFormat = ImageFormat.PNG, ImageFormat imageFormat = ImageFormat.PNG,
@@ -45,8 +49,8 @@ class IVideoService {
int maxWidth = 0, int maxWidth = 0,
int timeMs = 0, int timeMs = 0,
int quality = 10, int quality = 10,
}) { }) =>
return VideoThumbnail.thumbnailData( VideoThumbnail.thumbnailData(
video: video, video: video,
imageFormat: imageFormat, imageFormat: imageFormat,
maxHeight: maxHeight, maxHeight: maxHeight,
@@ -55,7 +59,7 @@ class IVideoService {
quality: quality, quality: quality,
); );
} }
}
/// Get instance of [IVideoService]
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
IVideoService get VideoService => IVideoService.instance; IVideoService get VideoService => IVideoService.instance;
@@ -4,19 +4,11 @@ import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:stream_chat_flutter/src/video_service.dart';
import 'stream_svg_icon.dart'; /// Widget for creating video thumbnail image
import 'video_service.dart';
class VideoThumbnailImage extends StatefulWidget { class VideoThumbnailImage extends StatefulWidget {
final String video; /// Constructor for creating [VideoThumbnailImage]
final double? width;
final double? height;
final BoxFit? fit;
final ImageFormat format;
final Widget Function(BuildContext, Object?)? errorBuilder;
final WidgetBuilder? placeholderBuilder;
const VideoThumbnailImage({ const VideoThumbnailImage({
Key? key, Key? key,
required this.video, required this.video,
@@ -28,6 +20,27 @@ class VideoThumbnailImage extends StatefulWidget {
this.placeholderBuilder, this.placeholderBuilder,
}) : super(key: key); }) : super(key: key);
/// Video path
final String video;
/// Width of widget
final double? width;
/// Height of widget
final double? height;
/// Fit of iamge
final BoxFit? fit;
/// Image format
final ImageFormat format;
/// Builds widget on error
final Widget Function(BuildContext, Object?)? errorBuilder;
/// Builds placeholder
final WidgetBuilder? placeholderBuilder;
@override @override
_VideoThumbnailImageState createState() => _VideoThumbnailImageState(); _VideoThumbnailImageState createState() => _VideoThumbnailImageState();
} }
@@ -56,11 +69,9 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => FutureBuilder<Uint8List?>(
return FutureBuilder<Uint8List?>(
future: thumbnailFuture, future: thumbnailFuture,
builder: (context, snapshot) { builder: (context, snapshot) => AnimatedSwitcher(
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
child: Builder( child: Builder(
key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot), key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot),
@@ -73,7 +84,7 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
} }
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Container( return Container(
constraints: BoxConstraints.expand(), constraints: const BoxConstraints.expand(),
child: widget.placeholderBuilder?.call(context) ?? child: widget.placeholderBuilder?.call(context) ??
Shimmer.fromColors( Shimmer.fromColors(
baseColor: StreamChatTheme.of(context) baseColor: StreamChatTheme.of(context)
@@ -97,8 +108,6 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
); );
}, },
), ),
); ),
},
); );
} }
}
+28 -28
View File
@@ -9,41 +9,41 @@ environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
cached_network_image: ^3.0.0
characters: ^1.1.0
chewie: ^1.0.0
collection: ^1.15.0
dio: ^4.0.0
ezanimation: ^0.5.0
file_picker: ^3.0.1
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter_core: ^2.0.0-nullsafety.2 flutter_keyboard_visibility: ^5.0.1
flutter_markdown: ^0.6.1
flutter_portal: ^0.4.0
flutter_slidable: ^0.6.0
flutter_svg: ^0.22.0
http_parser: ^4.0.0
image_gallery_saver: ^1.6.9
image_picker: ^0.7.4
jiffy: ^4.1.0
lottie: ^1.0.1
meta: ^1.3.0
path_provider: ^2.0.1
photo_manager: ^1.1.4
photo_view: ^0.11.1 photo_view: ^0.11.1
rxdart: ^0.26.0 rxdart: ^0.26.0
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
jiffy: ^4.1.0
flutter_svg: ^0.22.0
flutter_portal: ^0.4.0
cached_network_image: ^3.0.0
shimmer: ^2.0.0
flutter_markdown: ^0.6.1
url_launcher: ^6.0.3
video_player: ^2.1.1
chewie: ^1.0.0
file_picker: ^3.0.1
image_picker: ^0.7.4
flutter_keyboard_visibility: ^5.0.1
video_compress: ^3.0.0
visibility_detector: ^0.2.0
http_parser: ^4.0.0
meta: ^1.3.0
lottie: ^1.0.1
substring_highlight: ^1.0.26
flutter_slidable: ^0.6.0
image_gallery_saver: ^1.6.9
share_plus: ^2.0.3 share_plus: ^2.0.3
photo_manager: ^1.1.4 shimmer: ^2.0.0
ezanimation: ^0.5.0 stream_chat_flutter_core: ^2.0.0-nullsafety.2
substring_highlight: ^1.0.26
synchronized: ^3.0.0 synchronized: ^3.0.0
dio: ^4.0.0 url_launcher: ^6.0.3
characters: ^1.1.0 video_compress: ^3.0.0
path_provider: ^2.0.1 video_player: ^2.1.1
video_thumbnail: ^0.3.3 video_thumbnail: ^0.3.3
collection: ^1.15.0 visibility_detector: ^0.2.0
flutter: flutter:
assets: assets:
@@ -56,7 +56,7 @@ flutter:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
golden_toolkit: ^0.9.0
mocktail: ^0.1.2 mocktail: ^0.1.2
pedantic: ^1.11.0 pedantic: ^1.11.0
golden_toolkit: ^0.9.0
@@ -23,7 +23,8 @@ class MockAttachmentDownloader extends Mock {
void main() { void main() {
setUpAll(() { setUpAll(() {
registerFallbackValue(MaterialPageRoute(builder: (context) => SizedBox())); registerFallbackValue(
MaterialPageRoute(builder: (context) => const SizedBox()));
registerFallbackValue(Message()); registerFallbackValue(Message());
}); });
@@ -128,7 +129,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: Message( message: Message(
text: 'test', text: 'test',
@@ -185,7 +186,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: 0, currentIndex: 0,
@@ -218,7 +219,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
message: Message( message: Message(
@@ -239,7 +240,7 @@ void main() {
), ),
); );
await tester.tap(find.text('Show in Chat')); await tester.tap(find.text('Show in Chat'));
verify(() => onShowMessage.call()).called(1); verify(onShowMessage.call).called(1);
}, },
); );
@@ -250,9 +251,8 @@ void main() {
final clientState = MockClientState(); final clientState = MockClientState();
final mockChannel = MockChannel(); final mockChannel = MockChannel();
when(() => mockChannel.updateMessage(any())).thenAnswer((_) async { when(() => mockChannel.updateMessage(any()))
return UpdateMessageResponse(); .thenAnswer((_) async => UpdateMessageResponse());
});
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
@@ -275,12 +275,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
child: child!, child: child,
); ),
},
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: mockChannel, channel: mockChannel,
@@ -307,9 +305,8 @@ void main() {
final clientState = MockClientState(); final clientState = MockClientState();
final mockChannel = MockChannel(); final mockChannel = MockChannel();
when(() => mockChannel.updateMessage(any())).thenAnswer((_) async { when(() => mockChannel.updateMessage(any()))
return UpdateMessageResponse(); .thenAnswer((_) async => UpdateMessageResponse());
});
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
@@ -328,12 +325,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
child: child!, child: child,
); ),
},
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: mockChannel, channel: mockChannel,
@@ -352,15 +347,15 @@ void main() {
); );
testWidgets( testWidgets(
// ignore: lines_longer_than_80_chars
'tapping on delete in chat should remove the message if that\'s the only attachment and there is no text', 'tapping on delete in chat should remove the message if that\'s the only attachment and there is no text',
(WidgetTester tester) async { (WidgetTester tester) async {
final client = MockClient(); final client = MockClient();
final clientState = MockClientState(); final clientState = MockClientState();
final mockChannel = MockChannel(); final mockChannel = MockChannel();
when(() => mockChannel.deleteMessage(any())).thenAnswer((_) async { when(() => mockChannel.deleteMessage(any()))
return EmptyResponse(); .thenAnswer((_) async => EmptyResponse());
});
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
@@ -378,12 +373,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
child: child!, child: child,
); ),
},
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: mockChannel, channel: mockChannel,
@@ -412,13 +405,11 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
child: child!, child: child,
); ),
}, home: SizedBox(
home: Container(
child: AttachmentActionsModal( child: AttachmentActionsModal(
imageDownloader: imageDownloader, imageDownloader: imageDownloader,
message: Message( message: Message(
@@ -451,8 +442,8 @@ void main() {
imageDownloader.progressCallback!(100, 100); imageDownloader.progressCallback!(100, 100);
imageDownloader.completer.complete('path'); imageDownloader.completer.complete('path');
await tester.pump(); await tester.pump();
expect(find.byKey(Key('completedIcon')), findsOneWidget); expect(find.byKey(const Key('completedIcon')), findsOneWidget);
await tester.pumpAndSettle(Duration(milliseconds: 500)); await tester.pumpAndSettle(const Duration(milliseconds: 500));
}, },
); );
@@ -469,13 +460,11 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
child: child!, child: child,
); ),
}, home: SizedBox(
home: Container(
child: AttachmentActionsModal( child: AttachmentActionsModal(
fileDownloader: fileDownloader, fileDownloader: fileDownloader,
message: Message( message: Message(
@@ -508,8 +497,8 @@ void main() {
fileDownloader.progressCallback!(100, 100); fileDownloader.progressCallback!(100, 100);
fileDownloader.completer.complete('path'); fileDownloader.completer.complete('path');
await tester.pump(); await tester.pump();
expect(find.byKey(Key('completedIcon')), findsOneWidget); expect(find.byKey(const Key('completedIcon')), findsOneWidget);
await tester.pumpAndSettle(Duration(milliseconds: 500)); await tester.pumpAndSettle(const Duration(milliseconds: 500));
}, },
); );
} }
@@ -23,17 +23,17 @@ void main() {
data: streamTheme, data: streamTheme,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: FileAttachment( child: FileAttachment(
size: Size( size: const Size(
300.0, 300,
300.0, 300,
), ),
message: Message(), message: Message(),
attachment: Attachment( attachment: Attachment(
type: 'file', type: 'file',
title: 'example.pdf', title: 'example.pdf',
extraData: { extraData: const {
'mime_type': 'pdf', 'mime_type': 'pdf',
}, },
), ),
@@ -15,16 +15,14 @@ void main() {
MaterialApp( MaterialApp(
home: const Material(child: Text('Home')), home: const Material(child: Text('Home')),
routes: <String, WidgetBuilder>{ routes: <String, WidgetBuilder>{
'/next': (BuildContext context) { '/next': (BuildContext context) => Material(
return Material(
child: Center( child: Center(
child: StreamChatTheme( child: StreamChatTheme(
data: StreamChatThemeData.fromTheme(theme), data: StreamChatThemeData.fromTheme(theme),
child: StreamBackButton(), child: const StreamBackButton(),
),
), ),
), ),
);
},
}, },
), ),
); );
@@ -52,7 +50,7 @@ void main() {
child: Center( child: Center(
child: StreamChatTheme( child: StreamChatTheme(
data: StreamChatThemeData.fromTheme(theme), data: StreamChatThemeData.fromTheme(theme),
child: StreamBackButton(), child: const StreamBackButton(),
), ),
), ),
), ),
@@ -78,8 +76,7 @@ void main() {
MaterialApp( MaterialApp(
home: const Material(child: Text('Home')), home: const Material(child: Text('Home')),
routes: <String, WidgetBuilder>{ routes: <String, WidgetBuilder>{
'/next': (BuildContext context) { '/next': (BuildContext context) => Material(
return Material(
child: Center( child: Center(
child: StreamChatTheme( child: StreamChatTheme(
data: StreamChatThemeData.fromTheme(theme), data: StreamChatThemeData.fromTheme(theme),
@@ -88,8 +85,7 @@ void main() {
), ),
), ),
), ),
); ),
},
}, },
), ),
); );
@@ -131,7 +127,7 @@ void main() {
child: Center( child: Center(
child: StreamChat( child: StreamChat(
client: client, client: client,
child: StreamBackButton( child: const StreamBackButton(
showUnreads: true, showUnreads: true,
), ),
), ),
@@ -54,7 +54,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelHeader(), body: ChannelHeader(),
), ),
), ),
@@ -115,7 +115,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelHeader( body: ChannelHeader(
showConnectionStateTile: true, showConnectionStateTile: true,
), ),
@@ -178,7 +178,7 @@ void main() {
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
showLoading: false, showLoading: false,
child: Scaffold( child: const Scaffold(
body: ChannelHeader( body: ChannelHeader(
showConnectionStateTile: true, showConnectionStateTile: true,
), ),
@@ -242,7 +242,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelHeader( body: ChannelHeader(
leading: Text('leading'), leading: Text('leading'),
subtitle: Text('subtitle'), subtitle: Text('subtitle'),
@@ -314,11 +314,10 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelHeader( body: ChannelHeader(
showTypingIndicator: false, showTypingIndicator: false,
showBackButton: false, showBackButton: false,
showConnectionStateTile: false,
), ),
), ),
), ),
@@ -34,7 +34,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelImage(), body: ChannelImage(),
), ),
), ),
@@ -112,7 +112,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelImage(), body: ChannelImage(),
), ),
), ),
@@ -207,7 +207,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelImage(), body: ChannelImage(),
), ),
), ),
@@ -248,7 +248,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelImage( body: ChannelImage(
selected: true, selected: true,
), ),
@@ -257,7 +257,7 @@ void main() {
), ),
)); ));
expect(find.byKey(Key('selectedImage')), findsOneWidget); expect(find.byKey(const Key('selectedImage')), findsOneWidget);
}, },
); );
} }
@@ -21,7 +21,7 @@ void main() {
MaterialApp( MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: ChannelListHeader(), body: ChannelListHeader(),
), ),
), ),
@@ -51,7 +51,7 @@ void main() {
MaterialApp( MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: ChannelListHeader( body: ChannelListHeader(
showConnectionStateTile: true, showConnectionStateTile: true,
), ),
@@ -80,7 +80,7 @@ void main() {
MaterialApp( MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: ChannelListHeader( body: ChannelListHeader(
showConnectionStateTile: true, showConnectionStateTile: true,
), ),
@@ -111,12 +111,10 @@ void main() {
client: client, client: client,
child: Scaffold( child: Scaffold(
body: ChannelListHeader( body: ChannelListHeader(
titleBuilder: (context, status, client) { titleBuilder: (context, status, client) => const Text('TITLE'),
return Text('TITLE'); subtitle: const Text('SUBTITLE'),
}, leading: const Text('LEADING'),
subtitle: Text('SUBTITLE'), actions: const [
leading: Text('LEADING'),
actions: [
Text('ACTION'), Text('ACTION'),
], ],
client: client, client: client,
@@ -61,7 +61,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: ChannelName(), body: ChannelName(),
), ),
), ),
@@ -19,7 +19,7 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: DeletedMessage( body: DeletedMessage(
messageTheme: MessageTheme( messageTheme: MessageTheme(
createdAt: TextStyle( createdAt: TextStyle(
@@ -81,7 +81,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size.square(200), surfaceSize: const Size.square(200),
); );
await screenMatchesGolden(tester, 'deleted_message_light'); await screenMatchesGolden(tester, 'deleted_message_light');
@@ -133,7 +133,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size.square(200), surfaceSize: const Size.square(200),
); );
await screenMatchesGolden(tester, 'deleted_message_dark'); await screenMatchesGolden(tester, 'deleted_message_dark');
@@ -189,7 +189,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size.square(200), surfaceSize: const Size.square(200),
); );
await screenMatchesGolden(tester, 'deleted_message_custom'); await screenMatchesGolden(tester, 'deleted_message_custom');
@@ -19,9 +19,9 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: Portal( body: Portal(
child: Container( child: SizedBox(
child: InfoTile( child: InfoTile(
showMessage: true, showMessage: true,
message: 'message', message: 'message',
@@ -49,9 +49,9 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: Scaffold( child: const Scaffold(
body: Portal( body: Portal(
child: Container( child: SizedBox(
child: InfoTile( child: InfoTile(
showMessage: false, showMessage: false,
message: 'message', message: 'message',
@@ -8,7 +8,8 @@ import 'mocks.dart';
void main() { void main() {
setUpAll(() { setUpAll(() {
registerFallbackValue(MaterialPageRoute(builder: (context) => SizedBox())); registerFallbackValue(
MaterialPageRoute(builder: (context) => const SizedBox()));
registerFallbackValue(Message()); registerFallbackValue(Message());
}); });
@@ -29,7 +30,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
text: 'test', text: 'test',
@@ -45,7 +46,7 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(const Key('MessageWidget')), findsOneWidget);
expect(find.text('Thread Reply'), findsOneWidget); expect(find.text('Thread Reply'), findsOneWidget);
expect(find.text('Reply'), findsOneWidget); expect(find.text('Reply'), findsOneWidget);
expect(find.text('Edit Message'), findsOneWidget); expect(find.text('Edit Message'), findsOneWidget);
@@ -71,7 +72,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
showEditMessage: false, showEditMessage: false,
showCopyMessage: false, showCopyMessage: false,
@@ -92,7 +93,7 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(const Key('MessageWidget')), findsOneWidget);
expect(find.text('Reply'), findsNothing); expect(find.text('Reply'), findsNothing);
expect(find.text('Thread reply'), findsNothing); expect(find.text('Thread reply'), findsNothing);
expect(find.text('Edit message'), findsNothing); expect(find.text('Edit message'), findsNothing);
@@ -120,7 +121,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
text: 'test', text: 'test',
@@ -131,8 +132,8 @@ void main() {
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
customActions: [ customActions: [
MessageAction( MessageAction(
leading: Icon(Icons.check), leading: const Icon(Icons.check),
title: Text('title'), title: const Text('title'),
onTap: (m) { onTap: (m) {
tapped = true; tapped = true;
}, },
@@ -175,7 +176,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
onReplyTap: (m) { onReplyTap: (m) {
tapped = true; tapped = true;
@@ -220,7 +221,7 @@ void main() {
home: StreamChat( home: StreamChat(
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
onThreadReplyTap: (m) { onThreadReplyTap: (m) {
tapped = true; tapped = true;
@@ -260,18 +261,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
text: 'test', text: 'test',
@@ -310,22 +309,18 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
editMessageInputBuilder: (context, m) { editMessageInputBuilder: (context, m) => const Text('test'),
return Text('test');
},
message: Message( message: Message(
text: 'test', text: 'test',
user: User( user: User(
@@ -365,18 +360,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
onCopyTap: (m) => tapped = true, onCopyTap: (m) => tapped = true,
message: Message( message: Message(
@@ -416,18 +409,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
status: MessageSendingStatus.failed, status: MessageSendingStatus.failed,
@@ -467,18 +458,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
status: MessageSendingStatus.failed_update, status: MessageSendingStatus.failed_update,
@@ -516,18 +505,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
id: 'testid', id: 'testid',
@@ -575,18 +562,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
id: 'testid', id: 'testid',
@@ -634,18 +619,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
id: 'testid', id: 'testid',
@@ -689,18 +672,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
id: 'testid', id: 'testid',
@@ -748,18 +729,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) { builder: (context, child) => StreamChat(
return StreamChat(
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: child!, child: child,
); ),
},
theme: themeData, theme: themeData,
home: StreamChannel( home: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Container( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
message: Message( message: Message(
id: 'testid', id: 'testid',
@@ -67,7 +67,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: MessageInput(), body: MessageInput(),
), ),
), ),
@@ -76,7 +76,7 @@ void main() {
expect(find.byType(TextField), findsOneWidget); expect(find.byType(TextField), findsOneWidget);
expect(find.byType(StreamSvgIcon), findsNWidgets(8)); expect(find.byType(StreamSvgIcon), findsNWidgets(8));
expect(find.byKey(Key('messageInputText')), findsOneWidget); expect(find.byKey(const Key('messageInputText')), findsOneWidget);
}, },
); );
} }
@@ -41,7 +41,7 @@ void main() {
), ),
); );
await tester.pump(Duration(milliseconds: 1000)); await tester.pump(const Duration(milliseconds: 1000));
expect(find.byType(MessageWidget), findsOneWidget); expect(find.byType(MessageWidget), findsOneWidget);
final messageWidget = final messageWidget =
@@ -95,6 +95,7 @@ void main() {
], ],
); );
// ignore: prefer_function_declarations_over_variables
final onUserAvatarTap = (u) => print('ok'); final onUserAvatarTap = (u) => print('ok');
await tester.pumpWidget( await tester.pumpWidget(
@@ -109,16 +110,16 @@ void main() {
showUserAvatar: DisplayWidget.gone, showUserAvatar: DisplayWidget.gone,
reverse: true, reverse: true,
attachmentBorderRadiusGeometry: BorderRadius.circular(1), attachmentBorderRadiusGeometry: BorderRadius.circular(1),
attachmentShape: RoundedRectangleBorder(), attachmentShape: const RoundedRectangleBorder(),
showReactions: false, showReactions: false,
messageShape: RoundedRectangleBorder(), messageShape: const RoundedRectangleBorder(),
onUserAvatarTap: onUserAvatarTap, onUserAvatarTap: onUserAvatarTap,
), ),
), ),
), ),
); );
await tester.pump(Duration(milliseconds: 1000)); await tester.pump(const Duration(milliseconds: 1000));
expect(find.byType(MessageWidget), findsOneWidget); expect(find.byType(MessageWidget), findsOneWidget);
final messageWidget = final messageWidget =
@@ -130,8 +131,8 @@ void main() {
expect(messageWidget.showUserAvatar, DisplayWidget.gone); expect(messageWidget.showUserAvatar, DisplayWidget.gone);
expect(messageWidget.reverse, true); expect(messageWidget.reverse, true);
expect(messageWidget.showReactions, false); expect(messageWidget.showReactions, false);
expect(messageWidget.attachmentShape, RoundedRectangleBorder()); expect(messageWidget.attachmentShape, const RoundedRectangleBorder());
expect(messageWidget.shape, RoundedRectangleBorder()); expect(messageWidget.shape, const RoundedRectangleBorder());
expect(messageWidget.showReactions, false); expect(messageWidget.showReactions, false);
expect(messageWidget.showUsername, false); expect(messageWidget.showUsername, false);
expect(messageWidget.showThreadReplyIndicator, false); expect(messageWidget.showThreadReplyIndicator, false);
@@ -11,6 +11,7 @@ class MockChannel extends Mock implements Channel {
Future<bool> get initialized async => true; Future<bool> get initialized async => true;
@override @override
// ignore: prefer_expression_function_bodies
Future<void> keyStroke([String? parentId]) async { Future<void> keyStroke([String? parentId]) async {
return; return;
} }
@@ -16,7 +16,7 @@ void main() {
SimpleFrame( SimpleFrame(
child: StreamChatTheme( child: StreamChatTheme(
data: StreamChatThemeData(), data: StreamChatThemeData(),
child: Container( child: const SizedBox(
child: ReactionBubble( child: ReactionBubble(
reactions: [], reactions: [],
borderColor: Colors.black, borderColor: Colors.black,
@@ -26,7 +26,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size(100, 100), surfaceSize: const Size(100, 100),
); );
await screenMatchesGolden(tester, 'reaction_bubble_0'); await screenMatchesGolden(tester, 'reaction_bubble_0');
}, },
@@ -47,7 +47,7 @@ void main() {
StreamChat( StreamChat(
client: client, client: client,
streamChatThemeData: theme, streamChatThemeData: theme,
child: Container( child: SizedBox(
child: ReactionBubble( child: ReactionBubble(
reactions: [ reactions: [
Reaction( Reaction(
@@ -61,7 +61,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size(100, 100), surfaceSize: const Size(100, 100),
); );
await screenMatchesGolden(tester, 'reaction_bubble_like_light'); await screenMatchesGolden(tester, 'reaction_bubble_like_light');
}, },
@@ -97,7 +97,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size(100, 100), surfaceSize: const Size(100, 100),
); );
await screenMatchesGolden(tester, 'reaction_bubble_like_dark'); await screenMatchesGolden(tester, 'reaction_bubble_like_dark');
}, },
@@ -141,7 +141,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size(140, 140), surfaceSize: const Size(140, 140),
); );
await screenMatchesGolden(tester, 'reaction_bubble_3_light'); await screenMatchesGolden(tester, 'reaction_bubble_3_light');
}, },
@@ -185,7 +185,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size(140, 140), surfaceSize: const Size(140, 140),
); );
await screenMatchesGolden(tester, 'reaction_bubble_3_dark'); await screenMatchesGolden(tester, 'reaction_bubble_3_dark');
}, },
@@ -205,7 +205,7 @@ void main() {
StreamChat( StreamChat(
client: client, client: client,
streamChatThemeData: StreamChatThemeData.fromTheme(themeData), streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
child: Container( child: SizedBox(
child: ReactionBubble( child: ReactionBubble(
reactions: [ reactions: [
Reaction( Reaction(
@@ -224,14 +224,13 @@ void main() {
borderColor: Colors.red, borderColor: Colors.red,
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
maskColor: Colors.green, maskColor: Colors.green,
highlightOwnReactions: true,
reverse: true, reverse: true,
flipTail: true, flipTail: true,
tailCirclesSpacing: 4, tailCirclesSpacing: 4,
), ),
), ),
), ),
surfaceSize: Size(200, 200), surfaceSize: const Size(200, 200),
); );
await screenMatchesGolden(tester, 'reaction_bubble_2'); await screenMatchesGolden(tester, 'reaction_bubble_2');
@@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SimpleFrame extends StatelessWidget { class SimpleFrame extends StatelessWidget {
final Widget child;
const SimpleFrame({Key? key, required this.child}) : super(key: key); const SimpleFrame({Key? key, required this.child}) : super(key: key);
final Widget child;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Container(
return Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
@@ -16,4 +15,3 @@ class SimpleFrame extends StatelessWidget {
child: child, child: child,
); );
} }
}
@@ -102,7 +102,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size.square(200), surfaceSize: const Size.square(200),
); );
await screenMatchesGolden(tester, 'system_message_light'); await screenMatchesGolden(tester, 'system_message_light');
@@ -153,7 +153,7 @@ void main() {
), ),
), ),
), ),
surfaceSize: Size.square(200), surfaceSize: const Size.square(200),
); );
await screenMatchesGolden(tester, 'system_message_dark'); await screenMatchesGolden(tester, 'system_message_dark');
@@ -117,13 +117,13 @@ void main() {
child: Scaffold( child: Scaffold(
body: ThreadHeader( body: ThreadHeader(
parent: Message(), parent: Message(),
subtitle: Text('subtitle'), subtitle: const Text('subtitle'),
leading: Text('leading'), leading: const Text('leading'),
title: Text('title'), title: const Text('title'),
onTitleTap: () { onTitleTap: () {
tapped = true; tapped = true;
}, },
actions: [ actions: const [
Text('action'), Text('action'),
], ],
), ),
@@ -67,14 +67,14 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: TypingIndicator(), body: TypingIndicator(),
), ),
), ),
), ),
)); ));
expect(find.byKey(Key('typings')), findsOneWidget); expect(find.byKey(const Key('typings')), findsOneWidget);
}, },
); );
} }
@@ -36,7 +36,7 @@ void main() {
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: Scaffold( child: const Scaffold(
body: UnreadIndicator(), body: UnreadIndicator(),
), ),
), ),
@@ -357,9 +357,9 @@ class StreamChannelState extends State<StreamChannel> {
if (snapshot.hasError) { if (snapshot.hasError) {
var message = snapshot.error.toString(); var message = snapshot.error.toString();
if (snapshot.error is DioError) { if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError; final dioError = snapshot.error as DioError?;
if (dioError.type == DioErrorType.response) { if (dioError?.type == DioErrorType.response) {
message = dioError.message; message = dioError!.message;
} else { } else {
message = 'Check your connection and retry'; message = 'Check your connection and retry';
} }
@@ -1,145 +0,0 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- diagnostic_describe_all_properties
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_put_required_named_parameters_first
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter