Merge pull request #412 from GetStream/feat/ui-linter
feat: added lint changes, started migrating
This commit is contained in:
@@ -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
|
||||
@@ -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>{}}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+74
-67
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -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);
|
||||
|
||||
/// 
|
||||
@@ -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(
|
||||
'Let’s 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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// 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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user