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