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:
|
analyzer:
|
||||||
|
enable-experiment:
|
||||||
|
- extension-methods
|
||||||
exclude:
|
exclude:
|
||||||
- lib/**/*.g.dart
|
- lib/**/*.g.dart
|
||||||
|
- example/**
|
||||||
|
- lib/src/emoji
|
||||||
- lib/**/*.freezed.dart
|
- lib/**/*.freezed.dart
|
||||||
- example/*
|
- test/**
|
||||||
- test/*
|
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
# these rules are documented on and in the same order as
|
||||||
|
# the Dart Lint rules page to make maintenance easier
|
||||||
|
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
||||||
- always_use_package_imports
|
- always_use_package_imports
|
||||||
- avoid_empty_else
|
- avoid_empty_else
|
||||||
- avoid_relative_lib_imports
|
- avoid_relative_lib_imports
|
||||||
@@ -43,7 +49,6 @@ linter:
|
|||||||
- avoid_private_typedef_functions
|
- avoid_private_typedef_functions
|
||||||
- avoid_redundant_argument_values
|
- avoid_redundant_argument_values
|
||||||
- avoid_return_types_on_setters
|
- avoid_return_types_on_setters
|
||||||
- avoid_returning_null
|
|
||||||
- avoid_returning_null_for_void
|
- avoid_returning_null_for_void
|
||||||
- avoid_shadowing_type_parameters
|
- avoid_shadowing_type_parameters
|
||||||
- avoid_single_cascade_in_expression_statements
|
- avoid_single_cascade_in_expression_statements
|
||||||
@@ -139,8 +144,7 @@ linter:
|
|||||||
- package_names
|
- package_names
|
||||||
- sort_pub_dependencies
|
- sort_pub_dependencies
|
||||||
|
|
||||||
# To be added when null-safe:
|
- cast_nullable_to_non_nullable
|
||||||
# - cast_nullable_to_non_nullable
|
- unnecessary_null_checks
|
||||||
#- unnecessary_null_checks
|
- tighten_type_of_initializing_formals
|
||||||
# - tighten_type_of_initializing_formals
|
- null_check_on_nullable_type_parameter
|
||||||
# - null_check_on_nullable_type_parameter
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
- lib/**/*.g.dart
|
|
||||||
- lib/**/*.freezed.dart
|
|
||||||
- example/*
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
- always_use_package_imports
|
|
||||||
- avoid_empty_else
|
|
||||||
- avoid_relative_lib_imports
|
|
||||||
- avoid_slow_async_io
|
|
||||||
- avoid_types_as_parameter_names
|
|
||||||
- cancel_subscriptions
|
|
||||||
- close_sinks
|
|
||||||
- control_flow_in_finally
|
|
||||||
- empty_statements
|
|
||||||
- hash_and_equals
|
|
||||||
- invariant_booleans
|
|
||||||
- iterable_contains_unrelated_type
|
|
||||||
- list_remove_unrelated_type
|
|
||||||
- literal_only_boolean_expressions
|
|
||||||
- no_adjacent_strings_in_list
|
|
||||||
- no_duplicate_case_values
|
|
||||||
- no_logic_in_create_state
|
|
||||||
- prefer_void_to_null
|
|
||||||
- test_types_in_equals
|
|
||||||
- throw_in_finally
|
|
||||||
- unnecessary_statements
|
|
||||||
- unrelated_type_equality_checks
|
|
||||||
- omit_local_variable_types
|
|
||||||
- use_key_in_widget_constructors
|
|
||||||
- valid_regexps
|
|
||||||
- always_declare_return_types
|
|
||||||
- always_require_non_null_named_parameters
|
|
||||||
- annotate_overrides
|
|
||||||
- avoid_bool_literals_in_conditional_expressions
|
|
||||||
- avoid_catching_errors
|
|
||||||
- avoid_init_to_null
|
|
||||||
- avoid_null_checks_in_equality_operators
|
|
||||||
- avoid_positional_boolean_parameters
|
|
||||||
- avoid_private_typedef_functions
|
|
||||||
- avoid_redundant_argument_values
|
|
||||||
- avoid_return_types_on_setters
|
|
||||||
- avoid_returning_null_for_void
|
|
||||||
- avoid_shadowing_type_parameters
|
|
||||||
- avoid_single_cascade_in_expression_statements
|
|
||||||
- avoid_unnecessary_containers
|
|
||||||
- avoid_unused_constructor_parameters
|
|
||||||
- await_only_futures
|
|
||||||
- camel_case_extensions
|
|
||||||
- camel_case_types
|
|
||||||
- cascade_invocations
|
|
||||||
|
|
||||||
- constant_identifier_names
|
|
||||||
- curly_braces_in_flow_control_structures
|
|
||||||
- directives_ordering
|
|
||||||
- empty_catches
|
|
||||||
- empty_constructor_bodies
|
|
||||||
- exhaustive_cases
|
|
||||||
- file_names
|
|
||||||
- implementation_imports
|
|
||||||
- join_return_with_assignment
|
|
||||||
- leading_newlines_in_multiline_strings
|
|
||||||
- library_names
|
|
||||||
- library_prefixes
|
|
||||||
- lines_longer_than_80_chars
|
|
||||||
- missing_whitespace_between_adjacent_strings
|
|
||||||
- non_constant_identifier_names
|
|
||||||
- null_closures
|
|
||||||
- one_member_abstracts
|
|
||||||
- only_throw_errors
|
|
||||||
- package_api_docs
|
|
||||||
- package_prefixed_library_names
|
|
||||||
- parameter_assignments
|
|
||||||
- prefer_adjacent_string_concatenation
|
|
||||||
- prefer_asserts_in_initializer_lists
|
|
||||||
- prefer_asserts_with_message
|
|
||||||
- prefer_collection_literals
|
|
||||||
- prefer_conditional_assignment
|
|
||||||
- prefer_const_constructors
|
|
||||||
- prefer_const_constructors_in_immutables
|
|
||||||
- prefer_const_declarations
|
|
||||||
- prefer_const_literals_to_create_immutables
|
|
||||||
- prefer_constructors_over_static_methods
|
|
||||||
- prefer_contains
|
|
||||||
- prefer_equal_for_default_values
|
|
||||||
- prefer_expression_function_bodies
|
|
||||||
- prefer_final_fields
|
|
||||||
- prefer_final_in_for_each
|
|
||||||
- prefer_final_locals
|
|
||||||
- prefer_function_declarations_over_variables
|
|
||||||
- prefer_generic_function_type_aliases
|
|
||||||
- prefer_if_elements_to_conditional_expressions
|
|
||||||
- prefer_if_null_operators
|
|
||||||
- prefer_initializing_formals
|
|
||||||
- prefer_inlined_adds
|
|
||||||
- prefer_int_literals
|
|
||||||
- prefer_interpolation_to_compose_strings
|
|
||||||
- prefer_is_empty
|
|
||||||
- prefer_is_not_empty
|
|
||||||
- prefer_is_not_operator
|
|
||||||
- prefer_null_aware_operators
|
|
||||||
- prefer_single_quotes
|
|
||||||
- prefer_spread_collections
|
|
||||||
- prefer_typing_uninitialized_variables
|
|
||||||
- provide_deprecation_message
|
|
||||||
- public_member_api_docs
|
|
||||||
- recursive_getters
|
|
||||||
- sized_box_for_whitespace
|
|
||||||
- slash_for_doc_comments
|
|
||||||
- sort_child_properties_last
|
|
||||||
- sort_constructors_first
|
|
||||||
- sort_unnamed_constructors_first
|
|
||||||
|
|
||||||
- type_annotate_public_apis
|
|
||||||
- type_init_formals
|
|
||||||
- unnecessary_await_in_return
|
|
||||||
- unnecessary_brace_in_string_interps
|
|
||||||
- unnecessary_const
|
|
||||||
- unnecessary_getters_setters
|
|
||||||
- unnecessary_lambdas
|
|
||||||
- unnecessary_new
|
|
||||||
- unnecessary_null_aware_assignments
|
|
||||||
- unnecessary_null_in_if_null_operators
|
|
||||||
- unnecessary_nullable_for_final_variable_declarations
|
|
||||||
- unnecessary_parenthesis
|
|
||||||
- unnecessary_raw_strings
|
|
||||||
- unnecessary_string_escapes
|
|
||||||
- unnecessary_string_interpolations
|
|
||||||
- unnecessary_this
|
|
||||||
- use_is_even_rather_than_modulo
|
|
||||||
- use_late_for_private_fields_and_variables
|
|
||||||
- use_rethrow_when_possible
|
|
||||||
- use_setters_to_change_properties
|
|
||||||
- use_to_and_as_if_applicable
|
|
||||||
- package_names
|
|
||||||
- sort_pub_dependencies
|
|
||||||
|
|
||||||
# To be added when null-safe:
|
|
||||||
# - cast_nullable_to_non_nullable
|
|
||||||
#- unnecessary_null_checks
|
|
||||||
# - tighten_type_of_initializing_formals
|
|
||||||
# - null_check_on_nullable_type_parameter
|
|
||||||
@@ -646,7 +646,7 @@ class Channel {
|
|||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
type: type,
|
type: type,
|
||||||
user: user!,
|
user: user,
|
||||||
score: 1,
|
score: 1,
|
||||||
extraData: extraData,
|
extraData: extraData,
|
||||||
);
|
);
|
||||||
@@ -654,7 +654,7 @@ class Channel {
|
|||||||
// Inserting at the 0th index as it's the latest reaction
|
// Inserting at the 0th index as it's the latest reaction
|
||||||
latestReactions.insert(0, newReaction);
|
latestReactions.insert(0, newReaction);
|
||||||
final ownReactions = [...latestReactions]
|
final ownReactions = [...latestReactions]
|
||||||
..removeWhere((it) => it.userId != user.id);
|
..removeWhere((it) => it.userId != user!.id);
|
||||||
|
|
||||||
final newMessage = message.copyWith(
|
final newMessage = message.copyWith(
|
||||||
reactionCounts: {...message.reactionCounts ?? <String, int>{}}
|
reactionCounts: {...message.reactionCounts ?? <String, int>{}}
|
||||||
|
|||||||
@@ -1495,8 +1495,7 @@ class ClientState {
|
|||||||
Stream<OwnUser?> get userStream => _userController.stream;
|
Stream<OwnUser?> get userStream => _userController.stream;
|
||||||
|
|
||||||
/// The current user
|
/// The current user
|
||||||
Map<String?, User?> get users =>
|
Map<String?, User?> get users => _usersController.value!;
|
||||||
_usersController.value as Map<String?, User?>;
|
|
||||||
|
|
||||||
/// The current user as a stream
|
/// The current user as a stream
|
||||||
Stream<Map<String?, User?>> get usersStream => _usersController.stream;
|
Stream<Map<String?, User?>> get usersStream => _usersController.stream;
|
||||||
|
|||||||
@@ -78,10 +78,15 @@ abstract class ChatPersistenceClient {
|
|||||||
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
|
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
|
||||||
]);
|
]);
|
||||||
return ChannelState(
|
return ChannelState(
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
members: data[0] as List<Member>,
|
members: data[0] as List<Member>,
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
read: data[1] as List<Read>,
|
read: data[1] as List<Read>,
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
channel: data[2] as ChannelModel?,
|
channel: data[2] as ChannelModel?,
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
messages: data[3] as List<Message>,
|
messages: data[3] as List<Message>,
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
pinnedMessages: data[4] as List<Message>,
|
pinnedMessages: data[4] as List<Message>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class ChannelModel {
|
|||||||
|
|
||||||
/// Shortcut for channel name
|
/// Shortcut for channel name
|
||||||
String get name =>
|
String get name =>
|
||||||
extraData.containsKey('name') ? extraData['name'] as String : cid;
|
extraData.containsKey('name') ? extraData['name']! as String : cid;
|
||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class User {
|
|||||||
/// Shortcut for user name
|
/// Shortcut for user name
|
||||||
String get name {
|
String get name {
|
||||||
if (extraData.containsKey('name')) {
|
if (extraData.containsKey('name')) {
|
||||||
final name = extraData['name'] as String;
|
final name = extraData['name']! as String;
|
||||||
if (name.isNotEmpty) return name;
|
if (name.isNotEmpty) return name;
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
include: package:pedantic/analysis_options.yaml
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
enable-experiment:
|
|
||||||
- extension-methods
|
|
||||||
exclude:
|
|
||||||
- lib/**/*.g.dart
|
|
||||||
- example/**
|
|
||||||
- lib/src/emoji
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
# these rules are documented on and in the same order as
|
|
||||||
# the Dart Lint rules page to make maintenance easier
|
|
||||||
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
|
||||||
# - always_declare_return_types
|
|
||||||
# - always_specify_types
|
|
||||||
# - annotate_overrides
|
|
||||||
# - avoid_as
|
|
||||||
- avoid_empty_else
|
|
||||||
- avoid_init_to_null
|
|
||||||
- avoid_return_types_on_setters
|
|
||||||
- avoid_web_libraries_in_flutter
|
|
||||||
- await_only_futures
|
|
||||||
- camel_case_types
|
|
||||||
- cancel_subscriptions
|
|
||||||
- close_sinks
|
|
||||||
# - comment_references # we do not presume as to what people want to reference in their dartdocs
|
|
||||||
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
|
|
||||||
- control_flow_in_finally
|
|
||||||
- empty_constructor_bodies
|
|
||||||
- empty_statements
|
|
||||||
- hash_and_equals
|
|
||||||
- implementation_imports
|
|
||||||
# - invariant_booleans
|
|
||||||
# - iterable_contains_unrelated_type
|
|
||||||
- library_names
|
|
||||||
# - library_prefixes
|
|
||||||
# - list_remove_unrelated_type
|
|
||||||
# - literal_only_boolean_expressions
|
|
||||||
- non_constant_identifier_names
|
|
||||||
# - one_member_abstracts
|
|
||||||
# - only_throw_errors
|
|
||||||
# - overridden_fields
|
|
||||||
# - package_api_docs
|
|
||||||
- package_names
|
|
||||||
- package_prefixed_library_names
|
|
||||||
- prefer_is_not_empty
|
|
||||||
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
|
||||||
# - public_member_api_docs
|
|
||||||
- slash_for_doc_comments
|
|
||||||
# - sort_constructors_first
|
|
||||||
# - sort_unnamed_constructors_first
|
|
||||||
# - super_goes_last # no longer needed w/ Dart 2
|
|
||||||
- test_types_in_equals
|
|
||||||
- throw_in_finally
|
|
||||||
# - type_annotate_public_apis # subset of always_specify_types
|
|
||||||
- type_init_formals
|
|
||||||
# - unawaited_futures
|
|
||||||
- unnecessary_brace_in_string_interps
|
|
||||||
- unnecessary_getters_setters
|
|
||||||
- unnecessary_statements
|
|
||||||
- unrelated_type_equality_checks
|
|
||||||
- valid_regexps
|
|
||||||
@@ -58,7 +58,7 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
child: ChannelListView(
|
child: ChannelListView(
|
||||||
filter: Filter.in_(
|
filter: Filter.in_(
|
||||||
'members',
|
'members',
|
||||||
[StreamChat.of(context).user.id],
|
[StreamChat.of(context).user!.id],
|
||||||
),
|
),
|
||||||
sort: [SortOption('last_message_at')],
|
sort: [SortOption('last_message_at')],
|
||||||
pagination: PaginationParams(
|
pagination: PaginationParams(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
export 'attachment_upload_state_builder.dart';
|
||||||
|
export 'attachment_widget.dart'
|
||||||
|
show AttachmentError, AttachmentSource, AttachmentSourceX;
|
||||||
export 'file_attachment.dart';
|
export 'file_attachment.dart';
|
||||||
export 'giphy_attachment.dart';
|
export 'giphy_attachment.dart';
|
||||||
export 'image_attachment.dart';
|
export 'image_attachment.dart';
|
||||||
export 'video_attachment.dart';
|
export 'video_attachment.dart';
|
||||||
export 'attachment_upload_state_builder.dart';
|
|
||||||
export 'attachment_widget.dart'
|
|
||||||
show AttachmentError, AttachmentSource, AttachmentSourceX;
|
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../stream_chat_theme.dart';
|
/// Title for attachments
|
||||||
import '../utils.dart';
|
|
||||||
|
|
||||||
class AttachmentTitle extends StatelessWidget {
|
class AttachmentTitle extends StatelessWidget {
|
||||||
|
/// Supply attachment and theme for constructing title
|
||||||
const AttachmentTitle({
|
const AttachmentTitle({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.attachment,
|
required this.attachment,
|
||||||
required this.messageTheme,
|
required this.messageTheme,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Theme to apply to text
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Attachment data to display
|
||||||
final Attachment attachment;
|
final Attachment attachment;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (attachment.titleLink != null) {
|
if (attachment.titleLink != null) {
|
||||||
launchURL(context, attachment.titleLink!);
|
launchURL(context, attachment.titleLink);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -37,7 +39,8 @@ class AttachmentTitle extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
|
if (attachment.titleLink != null ||
|
||||||
|
attachment.ogScrapeUrl != null)
|
||||||
Text(
|
Text(
|
||||||
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!)
|
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!)
|
||||||
.authority
|
.authority
|
||||||
@@ -54,4 +57,3 @@ class AttachmentTitle extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+44
-37
@@ -2,17 +2,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Widget to build in progress
|
||||||
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||||
|
|
||||||
|
/// Widget to build on failure
|
||||||
typedef FailedBuilder = Widget Function(BuildContext, String);
|
typedef FailedBuilder = Widget Function(BuildContext, String);
|
||||||
|
|
||||||
|
/// Widget to display attachment upload state
|
||||||
class AttachmentUploadStateBuilder extends StatelessWidget {
|
class AttachmentUploadStateBuilder extends StatelessWidget {
|
||||||
final Message message;
|
/// Constructor for creating an [AttachmentUploadStateBuilder] widget
|
||||||
final Attachment attachment;
|
|
||||||
final FailedBuilder? failedBuilder;
|
|
||||||
final WidgetBuilder? successBuilder;
|
|
||||||
final InProgressBuilder? inProgressBuilder;
|
|
||||||
final WidgetBuilder? preparingBuilder;
|
|
||||||
|
|
||||||
const AttachmentUploadStateBuilder({
|
const AttachmentUploadStateBuilder({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -23,32 +21,46 @@ class AttachmentUploadStateBuilder extends StatelessWidget {
|
|||||||
this.preparingBuilder,
|
this.preparingBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Message which attachment is added to
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// Attachment in concern
|
||||||
|
final Attachment attachment;
|
||||||
|
|
||||||
|
/// Widget to display when failed
|
||||||
|
final FailedBuilder? failedBuilder;
|
||||||
|
|
||||||
|
/// Widget to display when succeeded
|
||||||
|
final WidgetBuilder? successBuilder;
|
||||||
|
|
||||||
|
/// Widget to display when in progress
|
||||||
|
final InProgressBuilder? inProgressBuilder;
|
||||||
|
|
||||||
|
/// Widget to display when in prep
|
||||||
|
final WidgetBuilder? preparingBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (message.status == MessageSendingStatus.sent) {
|
if (message.status == MessageSendingStatus.sent) {
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final messageId = message.id;
|
final messageId = message.id;
|
||||||
final attachmentId = attachment.id;
|
final attachmentId = attachment.id;
|
||||||
|
|
||||||
final inProgress = inProgressBuilder ??
|
final inProgress = inProgressBuilder ??
|
||||||
(context, int sent, int total) {
|
(context, int sent, int total) => _InProgressState(
|
||||||
return _InProgressState(
|
|
||||||
sent: sent,
|
sent: sent,
|
||||||
total: total,
|
total: total,
|
||||||
attachmentId: attachmentId,
|
attachmentId: attachmentId,
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
final failed = failedBuilder ??
|
final failed = failedBuilder ??
|
||||||
(context, error) {
|
(context, error) => _FailedState(
|
||||||
return _FailedState(
|
|
||||||
error: error,
|
error: error,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
attachmentId: attachmentId,
|
attachmentId: attachmentId,
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
final success = successBuilder ?? (context) => _SuccessState();
|
final success = successBuilder ?? (context) => _SuccessState();
|
||||||
|
|
||||||
@@ -65,11 +77,6 @@ class AttachmentUploadStateBuilder extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _IconButton extends StatelessWidget {
|
class _IconButton extends StatelessWidget {
|
||||||
final Widget? icon;
|
|
||||||
final double iconSize;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final Color? fillColor;
|
|
||||||
|
|
||||||
const _IconButton({
|
const _IconButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.icon,
|
this.icon,
|
||||||
@@ -78,16 +85,19 @@ class _IconButton extends StatelessWidget {
|
|||||||
this.fillColor,
|
this.fillColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget? icon;
|
||||||
|
final double iconSize;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final Color? fillColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SizedBox(
|
||||||
return Container(
|
|
||||||
height: iconSize,
|
height: iconSize,
|
||||||
width: iconSize,
|
width: iconSize,
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
highlightElevation: 0,
|
highlightElevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
disabledElevation: 0,
|
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
fillColor:
|
fillColor:
|
||||||
@@ -99,16 +109,15 @@ class _IconButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _PreparingState extends StatelessWidget {
|
class _PreparingState extends StatelessWidget {
|
||||||
final String attachmentId;
|
|
||||||
|
|
||||||
const _PreparingState({
|
const _PreparingState({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.attachmentId,
|
required this.attachmentId,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String attachmentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
@@ -138,10 +147,6 @@ class _PreparingState extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _InProgressState extends StatelessWidget {
|
class _InProgressState extends StatelessWidget {
|
||||||
final int sent;
|
|
||||||
final int total;
|
|
||||||
final String attachmentId;
|
|
||||||
|
|
||||||
const _InProgressState({
|
const _InProgressState({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.sent,
|
required this.sent,
|
||||||
@@ -149,6 +154,10 @@ class _InProgressState extends StatelessWidget {
|
|||||||
required this.attachmentId,
|
required this.attachmentId,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final int sent;
|
||||||
|
final int total;
|
||||||
|
final String attachmentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
@@ -178,10 +187,6 @@ class _InProgressState extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FailedState extends StatelessWidget {
|
class _FailedState extends StatelessWidget {
|
||||||
final String? error;
|
|
||||||
final String messageId;
|
|
||||||
final String attachmentId;
|
|
||||||
|
|
||||||
const _FailedState({
|
const _FailedState({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.error,
|
this.error,
|
||||||
@@ -189,6 +194,10 @@ class _FailedState extends StatelessWidget {
|
|||||||
required this.attachmentId,
|
required this.attachmentId,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String? error;
|
||||||
|
final String messageId;
|
||||||
|
final String attachmentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
@@ -232,16 +241,14 @@ class _FailedState extends StatelessWidget {
|
|||||||
|
|
||||||
class _SuccessState extends StatelessWidget {
|
class _SuccessState extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Align(
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||||
maxRadius: 12.0,
|
maxRadius: 12,
|
||||||
child: StreamSvgIcon.check(
|
child: StreamSvgIcon.check(
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../stream_chat_theme.dart';
|
/// Enum for identifying type of attachment
|
||||||
|
|
||||||
enum AttachmentSource {
|
enum AttachmentSource {
|
||||||
|
/// Attachment is attached
|
||||||
local,
|
local,
|
||||||
|
|
||||||
|
/// Attachment is uploaded
|
||||||
network,
|
network,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extension for identifying type of attachment
|
||||||
extension AttachmentSourceX on AttachmentSource {
|
extension AttachmentSourceX on AttachmentSource {
|
||||||
/// The [when] method is the equivalent to pattern matching.
|
/// The [when] method is the equivalent to pattern matching.
|
||||||
/// Its prototype depends on the AttachmentSource defined.
|
/// Its prototype depends on the AttachmentSource defined.
|
||||||
@@ -25,18 +29,9 @@ extension AttachmentSourceX on AttachmentSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abstract class for deriving attachment types
|
||||||
abstract class AttachmentWidget extends StatelessWidget {
|
abstract class AttachmentWidget extends StatelessWidget {
|
||||||
final Size? size;
|
/// Constructor for creating attachment widget
|
||||||
final AttachmentSource? _source;
|
|
||||||
final Message message;
|
|
||||||
final Attachment attachment;
|
|
||||||
|
|
||||||
AttachmentSource get source =>
|
|
||||||
_source ??
|
|
||||||
(attachment.file != null
|
|
||||||
? AttachmentSource.local
|
|
||||||
: AttachmentSource.network);
|
|
||||||
|
|
||||||
const AttachmentWidget({
|
const AttachmentWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -45,23 +40,43 @@ abstract class AttachmentWidget extends StatelessWidget {
|
|||||||
AttachmentSource? source,
|
AttachmentSource? source,
|
||||||
}) : _source = source,
|
}) : _source = source,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
/// Size of attachments
|
||||||
|
final Size? size;
|
||||||
|
final AttachmentSource? _source;
|
||||||
|
|
||||||
|
/// Message which attachment is attached to
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// Attachment to display
|
||||||
|
final Attachment attachment;
|
||||||
|
|
||||||
|
/// Getter for source of attachment
|
||||||
|
AttachmentSource get source =>
|
||||||
|
_source ??
|
||||||
|
(attachment.file != null
|
||||||
|
? AttachmentSource.local
|
||||||
|
: AttachmentSource.network);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widget for building in case of error
|
||||||
class AttachmentError extends StatelessWidget {
|
class AttachmentError extends StatelessWidget {
|
||||||
final Size? size;
|
/// Constructor for creating AttachmentError
|
||||||
|
|
||||||
const AttachmentError({
|
const AttachmentError({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.size,
|
this.size,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Size of error
|
||||||
|
final Size? size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Center(
|
||||||
return Center(
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
color:
|
||||||
|
StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.error_outline,
|
Icons.error_outline,
|
||||||
@@ -71,4 +86,3 @@ class AttachmentError extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||||
|
|
||||||
import '../upload_progress_indicator.dart';
|
// ignore: always_use_package_imports
|
||||||
import 'attachment_widget.dart';
|
import 'attachment_widget.dart';
|
||||||
|
|
||||||
|
/// Widget for displaying file attachments
|
||||||
class FileAttachment extends AttachmentWidget {
|
class FileAttachment extends AttachmentWidget {
|
||||||
final Widget? title;
|
/// Constructor for creating a widget when attachment is of type 'file'
|
||||||
final Widget? trailing;
|
|
||||||
final VoidCallback? onAttachmentTap;
|
|
||||||
|
|
||||||
const FileAttachment({
|
const FileAttachment({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Message message,
|
required Message message,
|
||||||
@@ -30,8 +29,19 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Title for attachment
|
||||||
|
final Widget? title;
|
||||||
|
|
||||||
|
/// Widget for displaying at the end of attachment (such as a download button)
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
/// Callback called when attachment widget is tapped
|
||||||
|
final VoidCallback? onAttachmentTap;
|
||||||
|
|
||||||
|
/// Check if attachment is a video
|
||||||
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
|
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
|
||||||
|
|
||||||
|
/// Check if attachment is an image
|
||||||
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
|
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,7 +52,7 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
onTap: onAttachmentTap,
|
onTap: onAttachmentTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: size?.width ?? 100,
|
width: size?.width ?? 100,
|
||||||
height: 56.0,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorTheme.white,
|
color: colorTheme.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -54,12 +64,12 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
height: 40.0,
|
height: 40,
|
||||||
width: 33.33,
|
width: 33.33,
|
||||||
margin: EdgeInsets.all(8.0),
|
margin: const EdgeInsets.all(8),
|
||||||
child: _getFileTypeImage(context),
|
child: _getFileTypeImage(context),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8.0),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -71,12 +81,12 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
SizedBox(height: 3.0),
|
const SizedBox(height: 3),
|
||||||
_buildSubtitle(context),
|
_buildSubtitle(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8.0),
|
const SizedBox(width: 8),
|
||||||
_buildTrailing(context),
|
_buildTrailing(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -85,12 +95,10 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
||||||
return RoundedRectangleBorder(
|
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||||
side: BorderSide(width: 0.0, color: Colors.transparent),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _getFileTypeImage(BuildContext context) {
|
Widget _getFileTypeImage(BuildContext context) {
|
||||||
if (isImageAttachment) {
|
if (isImageAttachment) {
|
||||||
@@ -106,10 +114,8 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
return Image.memory(
|
return Image.memory(
|
||||||
attachment.file!.bytes!,
|
attachment.file!.bytes!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, obj, trace) {
|
errorBuilder: (_, obj, trace) =>
|
||||||
return getFileTypeImage(
|
getFileTypeImage(attachment.extraData['other'] as String?),
|
||||||
attachment.extraData['other'] as String?);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
network: () {
|
network: () {
|
||||||
@@ -124,10 +130,8 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
attachment.assetUrl ??
|
attachment.assetUrl ??
|
||||||
attachment.thumbUrl!,
|
attachment.thumbUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: (_, obj, trace) {
|
errorWidget: (_, obj, trace) =>
|
||||||
return getFileTypeImage(
|
getFileTypeImage(attachment.extraData['other'] as String?),
|
||||||
attachment.extraData['other'] as String?);
|
|
||||||
},
|
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) {
|
||||||
final image = Image.asset(
|
final image = Image.asset(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
@@ -156,27 +160,23 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
child: source.when(
|
child: source.when(
|
||||||
local: () => VideoThumbnailImage(
|
local: () => VideoThumbnailImage(
|
||||||
video: attachment.file!.path!,
|
video: attachment.file!.path!,
|
||||||
placeholderBuilder: (_) {
|
placeholderBuilder: (_) => const Center(
|
||||||
return Center(
|
child: SizedBox(
|
||||||
child: Container(
|
width: 20,
|
||||||
width: 20.0,
|
height: 20,
|
||||||
height: 20.0,
|
child: CircularProgressIndicator(),
|
||||||
child: const CircularProgressIndicator(),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
network: () => VideoThumbnailImage(
|
network: () => VideoThumbnailImage(
|
||||||
video: attachment.assetUrl!,
|
video: attachment.assetUrl!,
|
||||||
placeholderBuilder: (_) {
|
placeholderBuilder: (_) => const Center(
|
||||||
return Center(
|
child: SizedBox(
|
||||||
child: Container(
|
width: 20,
|
||||||
width: 20.0,
|
height: 20,
|
||||||
height: 20.0,
|
child: CircularProgressIndicator(),
|
||||||
child: const CircularProgressIndicator(),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -189,23 +189,22 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
double iconSize = 24.0,
|
double iconSize = 24.0,
|
||||||
VoidCallback? onPressed,
|
VoidCallback? onPressed,
|
||||||
Color? fillColor,
|
Color? fillColor,
|
||||||
}) {
|
}) =>
|
||||||
return Container(
|
SizedBox(
|
||||||
height: iconSize,
|
height: iconSize,
|
||||||
width: iconSize,
|
width: iconSize,
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
highlightElevation: 0,
|
highlightElevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
disabledElevation: 0,
|
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
fillColor: fillColor,
|
fillColor: fillColor,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTrailing(BuildContext context) {
|
Widget _buildTrailing(BuildContext context) {
|
||||||
final theme = StreamChatTheme.of(context);
|
final theme = StreamChatTheme.of(context);
|
||||||
@@ -214,7 +213,7 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
var trailingWidget = trailing;
|
var trailingWidget = trailing;
|
||||||
trailingWidget ??= attachment.uploadState.when(
|
trailingWidget ??= attachment.uploadState.when(
|
||||||
preparing: () => Padding(
|
preparing: () => Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: _buildButton(
|
child: _buildButton(
|
||||||
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
||||||
fillColor: theme.colorTheme.overlayDark,
|
fillColor: theme.colorTheme.overlayDark,
|
||||||
@@ -222,7 +221,7 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
inProgress: (_, __) => Padding(
|
inProgress: (_, __) => Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: _buildButton(
|
child: _buildButton(
|
||||||
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
||||||
fillColor: theme.colorTheme.overlayDark,
|
fillColor: theme.colorTheme.overlayDark,
|
||||||
@@ -230,15 +229,15 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
success: () => Padding(
|
success: () => Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: theme.colorTheme.accentBlue,
|
backgroundColor: theme.colorTheme.accentBlue,
|
||||||
maxRadius: 12.0,
|
maxRadius: 12,
|
||||||
child: StreamSvgIcon.check(color: theme.colorTheme.white),
|
child: StreamSvgIcon.check(color: theme.colorTheme.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
failed: (_) => Padding(
|
failed: (_) => Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: _buildButton(
|
child: _buildButton(
|
||||||
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
|
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
|
||||||
fillColor: theme.colorTheme.overlayDark,
|
fillColor: theme.colorTheme.overlayDark,
|
||||||
@@ -251,7 +250,6 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
) ??
|
) ??
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
splashRadius: 16,
|
splashRadius: 16,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -262,7 +260,6 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
if (message.status == MessageSendingStatus.sent) {
|
if (message.status == MessageSendingStatus.sent) {
|
||||||
trailingWidget = IconButton(
|
trailingWidget = IconButton(
|
||||||
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
splashRadius: 16,
|
splashRadius: 16,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -284,29 +281,25 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
color: theme.colorTheme.grey,
|
color: theme.colorTheme.grey,
|
||||||
);
|
);
|
||||||
return attachment.uploadState.when(
|
return attachment.uploadState.when(
|
||||||
preparing: () {
|
preparing: () => UploadProgressIndicator(
|
||||||
return UploadProgressIndicator(
|
|
||||||
uploaded: 0,
|
uploaded: 0,
|
||||||
total: double.maxFinite.toInt(),
|
total: double.maxFinite.toInt(),
|
||||||
showBackground: false,
|
showBackground: false,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
progressIndicatorColor: theme.colorTheme.accentBlue,
|
progressIndicatorColor: theme.colorTheme.accentBlue,
|
||||||
);
|
),
|
||||||
},
|
inProgress: (sent, total) => UploadProgressIndicator(
|
||||||
inProgress: (sent, total) {
|
|
||||||
return UploadProgressIndicator(
|
|
||||||
uploaded: sent,
|
uploaded: sent,
|
||||||
total: total,
|
total: total,
|
||||||
showBackground: false,
|
showBackground: false,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
progressIndicatorColor: theme.colorTheme.accentBlue,
|
progressIndicatorColor: theme.colorTheme.accentBlue,
|
||||||
);
|
),
|
||||||
},
|
success: () => Text(fileSize(size), style: textStyle),
|
||||||
success: () => Text('${fileSize(size, 2)}', style: textStyle),
|
|
||||||
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
|
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
|
||||||
) ??
|
) ??
|
||||||
Text('${fileSize(size)}', style: textStyle);
|
Text(fileSize(size), style: textStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../full_screen_media.dart';
|
/// Widget for showing a GIF attachment
|
||||||
import '../stream_chat_theme.dart';
|
|
||||||
import '../stream_svg_icon.dart';
|
|
||||||
import 'attachment_widget.dart';
|
|
||||||
|
|
||||||
class GiphyAttachment extends AttachmentWidget {
|
class GiphyAttachment extends AttachmentWidget {
|
||||||
final ShowMessageCallback? onShowMessage;
|
/// Constructor for creating a [GiphyAttachment] widget
|
||||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
|
||||||
final VoidCallback? onAttachmentTap;
|
|
||||||
|
|
||||||
const GiphyAttachment({
|
const GiphyAttachment({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Message message,
|
required Message message,
|
||||||
@@ -28,12 +23,21 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Callback when attachment is returned to from other screens
|
||||||
|
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||||
|
|
||||||
|
/// Callback when attachment is tapped
|
||||||
|
final VoidCallback? onAttachmentTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final imageUrl =
|
final imageUrl =
|
||||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null) {
|
||||||
return AttachmentError();
|
return const AttachmentError();
|
||||||
}
|
}
|
||||||
if (attachment.actions.isNotEmpty) {
|
if (attachment.actions.isNotEmpty) {
|
||||||
return _buildSendingAttachment(context, imageUrl);
|
return _buildSendingAttachment(context, imageUrl);
|
||||||
@@ -50,12 +54,11 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
bottomRight: Radius.circular(0.0),
|
topLeft: Radius.circular(16),
|
||||||
topLeft: Radius.circular(16.0),
|
bottomLeft: Radius.circular(16),
|
||||||
bottomLeft: Radius.circular(16.0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -63,16 +66,16 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.giphyIcon(),
|
StreamSvgIcon.giphyIcon(),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
const Text(
|
||||||
'Giphy',
|
'Giphy',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (attachment.title != null)
|
if (attachment.title != null)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -91,25 +94,22 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => onAttachmentTap ?? _onImageTap(context),
|
onTap: () => onAttachmentTap ?? _onImageTap(context),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) => SizedBox(
|
||||||
return Container(
|
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) =>
|
||||||
return AttachmentError(size: size);
|
AttachmentError(size: size),
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -123,11 +123,9 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
height: 0.5,
|
height: 0.5,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -156,10 +154,10 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
.withOpacity(0.2),
|
.withOpacity(0.2),
|
||||||
height: 50.0,
|
height: 50,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -189,10 +187,10 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
.withOpacity(0.2),
|
.withOpacity(0.2),
|
||||||
height: 50.0,
|
height: 50,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -217,11 +215,11 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.0),
|
const SizedBox(height: 4),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -230,10 +228,10 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
.withOpacity(0.5),
|
.withOpacity(0.5),
|
||||||
size: 16.0,
|
size: 16,
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 8.0,
|
width: 8,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Only visible to you',
|
'Only visible to you',
|
||||||
@@ -275,8 +273,8 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
if (res != null) onReturnAction?.call(res);
|
if (res != null) onReturnAction?.call(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
|
||||||
return Container(
|
SizedBox(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final res =
|
final res =
|
||||||
@@ -314,9 +312,8 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) =>
|
||||||
return AttachmentError(size: size);
|
AttachmentError(size: size),
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -332,8 +329,8 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8.0,
|
horizontal: 8,
|
||||||
vertical: 4.0,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -359,4 +356,3 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../full_screen_media.dart';
|
/// Widget for showing an image attachment
|
||||||
import '../stream_chat_theme.dart';
|
|
||||||
import 'attachment_title.dart';
|
|
||||||
import 'attachment_widget.dart';
|
|
||||||
|
|
||||||
class ImageAttachment extends AttachmentWidget {
|
class ImageAttachment extends AttachmentWidget {
|
||||||
final MessageTheme messageTheme;
|
/// Constructor for creating a [ImageAttachment] widget
|
||||||
final bool showTitle;
|
|
||||||
final ShowMessageCallback? onShowMessage;
|
|
||||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
|
||||||
final VoidCallback? onAttachmentTap;
|
|
||||||
|
|
||||||
const ImageAttachment({
|
const ImageAttachment({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Message message,
|
required Message message,
|
||||||
@@ -33,9 +27,23 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// [MessageTheme] for showing image title
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Flag for showing title
|
||||||
|
final bool showTitle;
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Callback when attachment is returned to from other screens
|
||||||
|
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||||
|
|
||||||
|
/// Callback when attachment is tapped
|
||||||
|
final VoidCallback? onAttachmentTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => source.when(
|
||||||
return source.when(
|
|
||||||
local: () {
|
local: () {
|
||||||
if (attachment.localUri == null || attachment.file?.bytes == null) {
|
if (attachment.localUri == null || attachment.file?.bytes == null) {
|
||||||
return AttachmentError(size: size);
|
return AttachmentError(size: size);
|
||||||
@@ -47,12 +55,10 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, _, __) {
|
errorBuilder: (context, _, __) => Image.asset(
|
||||||
return Image.asset(
|
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
package: 'stream_chat_flutter',
|
package: 'stream_chat_flutter',
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -103,18 +109,15 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) => AttachmentError(size: size),
|
||||||
return AttachmentError(size: size);
|
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
|
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) =>
|
||||||
return ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(size!),
|
constraints: BoxConstraints.loose(size!),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -128,7 +131,8 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel =
|
||||||
|
StreamChannel.of(context).channel;
|
||||||
return StreamChannel(
|
return StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: FullScreenMedia(
|
child: FullScreenMedia(
|
||||||
@@ -146,7 +150,7 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
child: imageWidget,
|
child: imageWidget,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: AttachmentUploadStateBuilder(
|
child: AttachmentUploadStateBuilder(
|
||||||
message: message,
|
message: message,
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
@@ -167,4 +171,3 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'attachment_title.dart';
|
/// Widget for showing a video attachment
|
||||||
import 'attachment_upload_state_builder.dart';
|
|
||||||
import 'attachment_widget.dart';
|
|
||||||
|
|
||||||
class VideoAttachment extends AttachmentWidget {
|
class VideoAttachment extends AttachmentWidget {
|
||||||
final MessageTheme messageTheme;
|
/// Constructor for creating a [VideoAttachment] widget
|
||||||
final ShowMessageCallback? onShowMessage;
|
|
||||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
|
||||||
final VoidCallback? onAttachmentTap;
|
|
||||||
|
|
||||||
const VideoAttachment({
|
const VideoAttachment({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Message message,
|
required Message message,
|
||||||
@@ -29,9 +24,20 @@ class VideoAttachment extends AttachmentWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// [MessageTheme] for showing title
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Callback when attachment is returned to from other screens
|
||||||
|
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||||
|
|
||||||
|
/// Callback when attachment is tapped
|
||||||
|
final VoidCallback? onAttachmentTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => source.when(
|
||||||
return source.when(
|
|
||||||
local: () {
|
local: () {
|
||||||
if (attachment.file == null) {
|
if (attachment.file == null) {
|
||||||
return AttachmentError(size: size);
|
return AttachmentError(size: size);
|
||||||
@@ -63,10 +69,9 @@ class VideoAttachment extends AttachmentWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) {
|
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) =>
|
||||||
return ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(size ?? Size.infinite),
|
constraints: BoxConstraints.loose(size ?? Size.infinite),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -94,17 +99,17 @@ class VideoAttachment extends AttachmentWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
videoWidget,
|
videoWidget,
|
||||||
Center(
|
const Center(
|
||||||
child: Material(
|
child: Material(
|
||||||
shape: CircleBorder(),
|
shape: CircleBorder(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16),
|
||||||
child: Icon(Icons.play_arrow),
|
child: Icon(Icons.play_arrow),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: AttachmentUploadStateBuilder(
|
child: AttachmentUploadStateBuilder(
|
||||||
message: message,
|
message: message,
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
@@ -126,4 +131,3 @@ class VideoAttachment extends AttachmentWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import '../stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'extension.dart';
|
|
||||||
|
|
||||||
/// Callback to download an attachment asset
|
/// Callback to download an attachment asset
|
||||||
typedef AttachmentDownloader = Future<String> Function(
|
typedef AttachmentDownloader = Future<String> Function(
|
||||||
@@ -17,6 +16,16 @@ typedef AttachmentDownloader = Future<String> Function(
|
|||||||
|
|
||||||
/// Widget that shows the options in the gallery view
|
/// Widget that shows the options in the gallery view
|
||||||
class AttachmentActionsModal extends StatelessWidget {
|
class AttachmentActionsModal extends StatelessWidget {
|
||||||
|
/// Returns a new [AttachmentActionsModal]
|
||||||
|
const AttachmentActionsModal({
|
||||||
|
Key? key,
|
||||||
|
required this.currentIndex,
|
||||||
|
required this.message,
|
||||||
|
this.onShowMessage,
|
||||||
|
this.imageDownloader,
|
||||||
|
this.fileDownloader,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The message containing the attachments
|
/// The message containing the attachments
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
@@ -32,39 +41,28 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
/// Callback to provide download files
|
/// Callback to provide download files
|
||||||
final AttachmentDownloader? fileDownloader;
|
final AttachmentDownloader? fileDownloader;
|
||||||
|
|
||||||
/// Returns a new [AttachmentActionsModal]
|
|
||||||
const AttachmentActionsModal({
|
|
||||||
required this.currentIndex,
|
|
||||||
required this.message,
|
|
||||||
this.onShowMessage,
|
|
||||||
this.imageDownloader,
|
|
||||||
this.fileDownloader,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () => Navigator.maybePop(context),
|
onTap: () => Navigator.maybePop(context),
|
||||||
child: _buildPage(context),
|
child: _buildPage(context),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPage(context) {
|
Widget _buildPage(context) {
|
||||||
final theme = StreamChatTheme.of(context);
|
final theme = StreamChatTheme.of(context);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: kToolbarHeight),
|
const SizedBox(height: kToolbarHeight),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: MediaQuery.of(context).size.width * 0.5,
|
width: MediaQuery.of(context).size.width * 0.5,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16.0),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -73,7 +71,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
'Reply',
|
'Reply',
|
||||||
StreamSvgIcon.iconCurveLineLeftUp(
|
StreamSvgIcon.iconCurveLineLeftUp(
|
||||||
size: 24.0,
|
size: 24,
|
||||||
color: theme.colorTheme.grey,
|
color: theme.colorTheme.grey,
|
||||||
),
|
),
|
||||||
() {
|
() {
|
||||||
@@ -84,16 +82,17 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
'Show in Chat',
|
'Show in Chat',
|
||||||
StreamSvgIcon.eye(
|
StreamSvgIcon.eye(
|
||||||
size: 24.0,
|
size: 24,
|
||||||
color: theme.colorTheme.black,
|
color: theme.colorTheme.black,
|
||||||
),
|
),
|
||||||
onShowMessage,
|
onShowMessage,
|
||||||
),
|
),
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||||
StreamSvgIcon.iconSave(
|
StreamSvgIcon.iconSave(
|
||||||
size: 24.0,
|
size: 24,
|
||||||
color: theme.colorTheme.grey,
|
color: theme.colorTheme.grey,
|
||||||
),
|
),
|
||||||
() {
|
() {
|
||||||
@@ -144,7 +143,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
'Delete',
|
'Delete',
|
||||||
StreamSvgIcon.delete(
|
StreamSvgIcon.delete(
|
||||||
size: 24.0,
|
size: 24,
|
||||||
color: theme.colorTheme.accentRed,
|
color: theme.colorTheme.accentRed,
|
||||||
),
|
),
|
||||||
() {
|
() {
|
||||||
@@ -194,18 +193,18 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
VoidCallback? onTap, {
|
VoidCallback? onTap, {
|
||||||
Color? color,
|
Color? color,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) {
|
}) =>
|
||||||
return Material(
|
Material(
|
||||||
key: key,
|
key: key,
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
icon,
|
icon,
|
||||||
SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
@@ -218,7 +217,6 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDownloadProgressDialog(
|
Widget _buildDownloadProgressDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -247,7 +245,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: progress == null
|
child: progress == null
|
||||||
? Container(
|
? SizedBox(
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
child: StreamSvgIcon.error(
|
child: StreamSvgIcon.error(
|
||||||
@@ -255,15 +253,15 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: progress.toProgressIndicatorValue == 1.0
|
: progress.toProgressIndicatorValue == 1.0
|
||||||
? Container(
|
? SizedBox(
|
||||||
key: Key('completedIcon'),
|
key: const Key('completedIcon'),
|
||||||
height: 160,
|
height: 160,
|
||||||
width: 160,
|
width: 160,
|
||||||
child: StreamSvgIcon.check(
|
child: StreamSvgIcon.check(
|
||||||
color: theme.colorTheme.greyGainsboro,
|
color: theme.colorTheme.greyGainsboro,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: SizedBox(
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -271,7 +269,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
value: progress.toProgressIndicatorValue,
|
value: progress.toProgressIndicatorValue,
|
||||||
strokeWidth: 8.0,
|
strokeWidth: 8,
|
||||||
valueColor: AlwaysStoppedAnimation(
|
valueColor: AlwaysStoppedAnimation(
|
||||||
theme.colorTheme.accentBlue,
|
theme.colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
@@ -317,14 +315,13 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DownloadProgress {
|
class _DownloadProgress {
|
||||||
final int total;
|
|
||||||
final int received;
|
|
||||||
|
|
||||||
const _DownloadProgress(this.total, this.received);
|
const _DownloadProgress(this.total, this.received);
|
||||||
|
|
||||||
factory _DownloadProgress.initial() {
|
factory _DownloadProgress.initial() =>
|
||||||
return _DownloadProgress(double.maxFinite.toInt(), 0);
|
_DownloadProgress(double.maxFinite.toInt(), 0);
|
||||||
}
|
|
||||||
|
final int total;
|
||||||
|
final int received;
|
||||||
|
|
||||||
double get toProgressIndicatorValue => received / total;
|
double get toProgressIndicatorValue => received / total;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Back button implementation
|
||||||
|
|
||||||
class StreamBackButton extends StatelessWidget {
|
class StreamBackButton extends StatelessWidget {
|
||||||
|
/// Constructor for creating back button
|
||||||
const StreamBackButton({
|
const StreamBackButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
@@ -12,15 +13,17 @@ class StreamBackButton extends StatelessWidget {
|
|||||||
this.cid,
|
this.cid,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Callback for when button is pressed
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
/// Show unread count
|
||||||
final bool showUnreads;
|
final bool showUnreads;
|
||||||
|
|
||||||
/// Channel cid used to retrieve unread count
|
/// Channel cid used to retrieve unread count
|
||||||
final String? cid;
|
final String? cid;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Stack(
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
RawMaterialButton(
|
RawMaterialButton(
|
||||||
@@ -30,7 +33,6 @@ class StreamBackButton extends StatelessWidget {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
highlightElevation: 0,
|
highlightElevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
disabledElevation: 0,
|
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (onPressed != null) {
|
if (onPressed != null) {
|
||||||
@@ -39,7 +41,7 @@ class StreamBackButton extends StatelessWidget {
|
|||||||
Navigator.maybePop(context);
|
Navigator.maybePop(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
padding: const EdgeInsets.all(14.0),
|
padding: const EdgeInsets.all(14),
|
||||||
child: StreamSvgIcon.left(
|
child: StreamSvgIcon.left(
|
||||||
size: 24,
|
size: 24,
|
||||||
color: StreamChatTheme.of(context).colorTheme.black,
|
color: StreamChatTheme.of(context).colorTheme.black,
|
||||||
@@ -56,4 +58,3 @@ class StreamBackButton extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Bottom Sheet with options
|
||||||
import 'channel_info.dart';
|
|
||||||
import 'option_list_tile.dart';
|
|
||||||
|
|
||||||
class ChannelBottomSheet extends StatefulWidget {
|
class ChannelBottomSheet extends StatefulWidget {
|
||||||
final VoidCallback? onViewInfoTap;
|
/// Constructor for creating bottom sheet
|
||||||
|
const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key);
|
||||||
|
|
||||||
ChannelBottomSheet({this.onViewInfoTap});
|
/// Callback when 'View Info' is tapped
|
||||||
|
final VoidCallback? onViewInfoTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
|
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
|
||||||
@@ -29,31 +30,31 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
return Material(
|
return Material(
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16.0),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: !_showActions
|
child: !_showActions
|
||||||
? SizedBox()
|
? const SizedBox()
|
||||||
: ListView(
|
: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 24.0,
|
height: 24,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: ChannelName(
|
child: ChannelName(
|
||||||
textStyle:
|
textStyle:
|
||||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 5.0,
|
height: 5,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: ChannelInfo(
|
child: ChannelInfo(
|
||||||
@@ -64,8 +65,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
.subtitle,
|
.subtitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 17.0,
|
height: 17,
|
||||||
),
|
),
|
||||||
if (channel.isDistinct && channel.memberCount == 2)
|
if (channel.isDistinct && channel.memberCount == 2)
|
||||||
Column(
|
Column(
|
||||||
@@ -75,16 +76,16 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
.firstWhere(
|
.firstWhere(
|
||||||
(e) => e.user?.id != userAsMember.user?.id)
|
(e) => e.user?.id != userAsMember.user?.id)
|
||||||
.user!,
|
.user!,
|
||||||
constraints: BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxHeight: 64.0,
|
maxHeight: 64,
|
||||||
maxWidth: 64.0,
|
maxWidth: 64,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(32.0),
|
borderRadius: BorderRadius.circular(32),
|
||||||
onlineIndicatorConstraints:
|
onlineIndicatorConstraints:
|
||||||
BoxConstraints.tight(Size(12.0, 12.0)),
|
BoxConstraints.tight(const Size(12, 12)),
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 6.0,
|
height: 6,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
members
|
members
|
||||||
@@ -102,29 +103,28 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
),
|
),
|
||||||
if (!(channel.isDistinct && channel.memberCount == 2))
|
if (!(channel.isDistinct && channel.memberCount == 2))
|
||||||
Container(
|
Container(
|
||||||
height: 94.0,
|
height: 94,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: members.length,
|
itemCount: members.length,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) => Padding(
|
||||||
return Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
user: members[index].user!,
|
user: members[index].user!,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 64.0,
|
height: 64,
|
||||||
width: 64.0,
|
width: 64,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(32.0),
|
borderRadius: BorderRadius.circular(32),
|
||||||
onlineIndicatorConstraints:
|
onlineIndicatorConstraints:
|
||||||
BoxConstraints.tight(Size(12.0, 12.0)),
|
BoxConstraints.tight(const Size(12, 12)),
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 6.0,
|
height: 6,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
members[index].user?.name ?? '',
|
members[index].user?.name ?? '',
|
||||||
@@ -136,16 +136,15 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
),
|
||||||
height: 24.0,
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
),
|
),
|
||||||
OptionListTile(
|
OptionListTile(
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: StreamSvgIcon.user(
|
child: StreamSvgIcon.user(
|
||||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
),
|
),
|
||||||
@@ -156,7 +155,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
if (!channel.isDistinct)
|
if (!channel.isDistinct)
|
||||||
OptionListTile(
|
OptionListTile(
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: StreamSvgIcon.userRemove(
|
child: StreamSvgIcon.userRemove(
|
||||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
),
|
),
|
||||||
@@ -175,7 +174,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
if (isOwner)
|
if (isOwner)
|
||||||
OptionListTile(
|
OptionListTile(
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: StreamSvgIcon.delete(
|
child: StreamSvgIcon.delete(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
),
|
),
|
||||||
@@ -195,7 +194,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
),
|
),
|
||||||
OptionListTile(
|
OptionListTile(
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: StreamSvgIcon.closeSmall(
|
child: StreamSvgIcon.closeSmall(
|
||||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,13 +4,9 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
|
|||||||
import 'package:stream_chat_flutter/src/channel_name.dart';
|
import 'package:stream_chat_flutter/src/channel_name.dart';
|
||||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import './channel_name.dart';
|
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
import 'channel_image.dart';
|
|
||||||
import 'connection_status_builder.dart';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
@@ -43,16 +39,36 @@ import 'connection_status_builder.dart';
|
|||||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||||
/// However you can also use it as a normal widget.
|
/// However you can also use it as a normal widget.
|
||||||
///
|
///
|
||||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
|
/// Make sure to have a [StreamChannel] ancestor in order to provide the
|
||||||
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
|
/// information about the channel.
|
||||||
|
/// Every part of the widget uses a [StreamBuilder] to render the channel
|
||||||
|
/// information as soon as it updates.
|
||||||
///
|
///
|
||||||
/// By default the widget shows a backButton that calls [Navigator.pop].
|
/// By default the widget shows a backButton that calls [Navigator.pop].
|
||||||
/// You can disable this button using the [showBackButton] property of just override the behaviour
|
/// You can disable this button using the [showBackButton] property of just
|
||||||
|
/// override the behaviour
|
||||||
/// with [onBackPressed].
|
/// with [onBackPressed].
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
/// The widget components render the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
/// Creates a channel header
|
||||||
|
const ChannelHeader({
|
||||||
|
Key? key,
|
||||||
|
this.showBackButton = true,
|
||||||
|
this.onBackPressed,
|
||||||
|
this.onTitleTap,
|
||||||
|
this.showTypingIndicator = true,
|
||||||
|
this.onImageTap,
|
||||||
|
this.showConnectionStateTile = false,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.actions,
|
||||||
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// True if this header shows the leading back button
|
/// True if this header shows the leading back button
|
||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
|
|
||||||
@@ -69,6 +85,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// If true the typing indicator will be rendered if a user is typing
|
/// If true the typing indicator will be rendered if a user is typing
|
||||||
final bool showTypingIndicator;
|
final bool showTypingIndicator;
|
||||||
|
|
||||||
|
/// Show connection tile on header
|
||||||
final bool showConnectionStateTile;
|
final bool showConnectionStateTile;
|
||||||
|
|
||||||
/// Title widget
|
/// Title widget
|
||||||
@@ -84,22 +101,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// By default it shows the [ChannelImage]
|
/// By default it shows the [ChannelImage]
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
/// Creates a channel header
|
|
||||||
ChannelHeader({
|
|
||||||
Key? key,
|
|
||||||
this.showBackButton = true,
|
|
||||||
this.onBackPressed,
|
|
||||||
this.onTitleTap,
|
|
||||||
this.showTypingIndicator = true,
|
|
||||||
this.onImageTap,
|
|
||||||
this.showConnectionStateTile = false,
|
|
||||||
this.title,
|
|
||||||
this.subtitle,
|
|
||||||
this.leading,
|
|
||||||
this.actions,
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
@@ -110,7 +111,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
showUnreads: true,
|
showUnreads: true,
|
||||||
)
|
)
|
||||||
: SizedBox());
|
: const SizedBox());
|
||||||
|
|
||||||
return ConnectionStatusBuilder(
|
return ConnectionStatusBuilder(
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
@@ -131,6 +132,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
showMessage: showConnectionStateTile ? showStatus : false,
|
showMessage: showConnectionStateTile ? showStatus : false,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@@ -144,7 +146,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
actions: actions ??
|
actions: actions ??
|
||||||
<Widget>[
|
<Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
padding: const EdgeInsets.only(right: 10),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ChannelImage(
|
child: ChannelImage(
|
||||||
borderRadius: StreamChatTheme.of(context)
|
borderRadius: StreamChatTheme.of(context)
|
||||||
@@ -165,11 +167,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: InkWell(
|
title: InkWell(
|
||||||
onTap: onTitleTap,
|
onTap: onTitleTap,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: preferredSize.height,
|
height: preferredSize.height,
|
||||||
width: preferredSize.width,
|
width: preferredSize.width,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
title ??
|
title ??
|
||||||
@@ -179,7 +180,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
.channelHeaderTheme
|
.channelHeaderTheme
|
||||||
.title,
|
.title,
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
subtitle ??
|
subtitle ??
|
||||||
ChannelInfo(
|
ChannelInfo(
|
||||||
showTypingIndicator: showTypingIndicator,
|
showTypingIndicator: showTypingIndicator,
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
/// The widget uses a [StreamBuilder] to render the channel information
|
||||||
|
/// image as soon as it updates.
|
||||||
///
|
///
|
||||||
/// By default the widget radius size is 40x40 pixels.
|
/// By default the widget radius size is 40x40 pixels.
|
||||||
/// Set the property [constraints] to set a custom dimension.
|
/// Set the property [constraints] to set a custom dimension.
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget renders the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelImage extends StatelessWidget {
|
class ChannelImage extends StatelessWidget {
|
||||||
/// Instantiate a new ChannelImage
|
/// Instantiate a new ChannelImage
|
||||||
@@ -56,6 +58,7 @@ class ChannelImage extends StatelessWidget {
|
|||||||
this.selectionThickness = 4,
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// [BorderRadius] to display the widget
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
/// The channel to show the image of
|
/// The channel to show the image of
|
||||||
@@ -67,10 +70,13 @@ class ChannelImage extends StatelessWidget {
|
|||||||
/// The function called when the image is tapped
|
/// The function called when the image is tapped
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
/// If image is selected
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
|
||||||
|
/// Selection color for image
|
||||||
final Color? selectionColor;
|
final Color? selectionColor;
|
||||||
|
|
||||||
|
/// Thickness of selection image
|
||||||
final double selectionThickness;
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -91,8 +97,7 @@ class ChannelImage extends StatelessWidget {
|
|||||||
stream: streamChat.client.state.usersStream.map(
|
stream: streamChat.client.state.usersStream.map(
|
||||||
(users) => users[otherMember?.userId] ?? otherMember!.user!),
|
(users) => users[otherMember?.userId] ?? otherMember!.user!),
|
||||||
initialData: otherMember!.user,
|
initialData: otherMember!.user,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => UserAvatar(
|
||||||
return UserAvatar(
|
|
||||||
borderRadius: borderRadius ??
|
borderRadius: borderRadius ??
|
||||||
StreamChatTheme.of(context)
|
StreamChatTheme.of(context)
|
||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
@@ -109,14 +114,14 @@ class ChannelImage extends StatelessWidget {
|
|||||||
selectionColor: selectionColor ??
|
selectionColor: selectionColor ??
|
||||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
selectionThickness: selectionThickness,
|
selectionThickness: selectionThickness,
|
||||||
);
|
));
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
final images = channel.state?.members
|
final images = channel.state?.members
|
||||||
.where((member) =>
|
.where((member) =>
|
||||||
member.user?.id != streamChat.user?.id &&
|
member.user?.id != streamChat.user?.id &&
|
||||||
member.user?.extraData['image'] != null)
|
member.user?.extraData['image'] != null)
|
||||||
.take(4)
|
.take(4)
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
.map((e) => e.user?.extraData['image'] as String)
|
.map((e) => e.user?.extraData['image'] as String)
|
||||||
.toList();
|
.toList();
|
||||||
return GroupImage(
|
return GroupImage(
|
||||||
@@ -158,27 +163,24 @@ class ChannelImage extends StatelessWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
image != null
|
if (image != null)
|
||||||
? CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
errorWidget: (_, __, ___) {
|
errorWidget: (_, __, ___) => Center(
|
||||||
return Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data?.containsKey('name') ?? false
|
snapshot.data?.containsKey('name') ?? false
|
||||||
? snapshot.data!['name'][0]
|
? snapshot.data!['name'][0]
|
||||||
: '',
|
: '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
.colorTheme
|
|
||||||
.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: StreamChatTheme.of(context).defaultChannelImage(
|
else
|
||||||
|
StreamChatTheme.of(context).defaultChannelImage(
|
||||||
context,
|
context,
|
||||||
channel,
|
channel,
|
||||||
),
|
),
|
||||||
@@ -194,7 +196,7 @@ class ChannelImage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
child = ClipRRect(
|
child = ClipRRect(
|
||||||
key: Key('selectedImage'),
|
key: const Key('selectedImage'),
|
||||||
borderRadius: (borderRadius ??
|
borderRadius: (borderRadius ??
|
||||||
StreamChatTheme.of(context)
|
StreamChatTheme.of(context)
|
||||||
.ownMessageTheme
|
.ownMessageTheme
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'connection_status_builder.dart';
|
/// Widget which shows channel info
|
||||||
|
|
||||||
class ChannelInfo extends StatelessWidget {
|
class ChannelInfo extends StatelessWidget {
|
||||||
|
/// Constructor which creates a [ChannelInfo] widget
|
||||||
|
const ChannelInfo({
|
||||||
|
Key? key,
|
||||||
|
required this.channel,
|
||||||
|
this.textStyle,
|
||||||
|
this.showTypingIndicator = true,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The channel about which the info is to be displayed
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
/// The style of the text displayed
|
/// The style of the text displayed
|
||||||
@@ -14,21 +22,13 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
/// If true the typing indicator will be rendered if a user is typing
|
/// If true the typing indicator will be rendered if a user is typing
|
||||||
final bool showTypingIndicator;
|
final bool showTypingIndicator;
|
||||||
|
|
||||||
const ChannelInfo({
|
|
||||||
Key? key,
|
|
||||||
required this.channel,
|
|
||||||
this.textStyle,
|
|
||||||
this.showTypingIndicator = true,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return StreamBuilder<List<Member>>(
|
return StreamBuilder<List<Member>>(
|
||||||
stream: channel.state?.membersStream,
|
stream: channel.state?.membersStream,
|
||||||
initialData: channel.state?.members,
|
initialData: channel.state?.members,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => ConnectionStatusBuilder(
|
||||||
return ConnectionStatusBuilder(
|
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
@@ -38,17 +38,16 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
return _buildDisconnectedTitleState(context, client);
|
return _buildDisconnectedTitleState(context, client);
|
||||||
default:
|
default:
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildConnectedTitleState(
|
Widget _buildConnectedTitleState(
|
||||||
BuildContext context, List<Member>? members) {
|
BuildContext context, List<Member>? members) {
|
||||||
var alternativeWidget;
|
Widget? alternativeWidget;
|
||||||
|
|
||||||
if (channel.memberCount != null && channel.memberCount! > 2) {
|
if (channel.memberCount != null && channel.memberCount! > 2) {
|
||||||
var text = '${channel.memberCount} Members';
|
var text = '${channel.memberCount} Members';
|
||||||
@@ -82,7 +81,7 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!showTypingIndicator) {
|
if (!showTypingIndicator) {
|
||||||
return alternativeWidget ?? Offstage();
|
return alternativeWidget ?? const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypingIndicator(
|
return TypingIndicator(
|
||||||
@@ -92,31 +91,29 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildConnectingTitleState(BuildContext context) {
|
Widget _buildConnectingTitleState(BuildContext context) => Row(
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 16,
|
width: 16,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'Searching for Network',
|
'Searching for Network',
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDisconnectedTitleState(
|
Widget _buildDisconnectedTitleState(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
StreamChatClient client,
|
StreamChatClient client,
|
||||||
) {
|
) =>
|
||||||
return Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -127,7 +124,7 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
visualDensity: VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
horizontal: VisualDensity.minimumDensity,
|
horizontal: VisualDensity.minimumDensity,
|
||||||
vertical: VisualDensity.minimumDensity,
|
vertical: VisualDensity.minimumDensity,
|
||||||
),
|
),
|
||||||
@@ -146,4 +143,3 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'connection_status_builder.dart';
|
/// Widget builder for title
|
||||||
import 'info_tile.dart';
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
|
|
||||||
typedef TitleBuilder = Widget Function(
|
typedef TitleBuilder = Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ConnectionStatus status,
|
ConnectionStatus status,
|
||||||
@@ -42,10 +39,13 @@ typedef TitleBuilder = Widget Function(
|
|||||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||||
/// However you can also use it as a normal widget.
|
/// However you can also use it as a normal widget.
|
||||||
///
|
///
|
||||||
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
|
/// The widget by default uses the inherited [StreamChatClient]
|
||||||
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree.
|
/// to fetch information about the status.
|
||||||
|
/// However you can also pass your own [StreamChatClient]
|
||||||
|
/// if you don't have it in the widget tree.
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelListHeaderTheme] property.
|
/// The widget components render the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme] and on its [ChannelListHeaderTheme] property.
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Instantiates a ChannelListHeader
|
/// Instantiates a ChannelListHeader
|
||||||
@@ -75,8 +75,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// Callback to call when pressing the new chat button.
|
/// Callback to call when pressing the new chat button.
|
||||||
final VoidCallback? onNewChatButtonTap;
|
final VoidCallback? onNewChatButtonTap;
|
||||||
|
|
||||||
|
/// Show connection state tile
|
||||||
final bool showConnectionStateTile;
|
final bool showConnectionStateTile;
|
||||||
|
|
||||||
|
/// Callback before navigation is performed
|
||||||
final VoidCallback? preNavigationCallback;
|
final VoidCallback? preNavigationCallback;
|
||||||
|
|
||||||
/// Subtitle widget
|
/// Subtitle widget
|
||||||
@@ -113,6 +115,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
showMessage: showConnectionStateTile ? showStatus : false,
|
showMessage: showConnectionStateTile ? showStatus : false,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@@ -143,7 +146,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
.avatarTheme
|
.avatarTheme
|
||||||
?.constraints,
|
?.constraints,
|
||||||
)
|
)
|
||||||
: Offstage(),
|
: const Offstage(),
|
||||||
),
|
),
|
||||||
actions: actions ??
|
actions: actions ??
|
||||||
[
|
[
|
||||||
@@ -151,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: ConnectionStatusBuilder(
|
icon: ConnectionStatusBuilder(
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
var color;
|
Color? color;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
color = StreamChatTheme.of(context)
|
color = StreamChatTheme.of(context)
|
||||||
@@ -168,8 +171,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
return SvgPicture.asset(
|
return SvgPicture.asset(
|
||||||
'svgs/icon_pen_write.svg',
|
'svgs/icon_pen_write.svg',
|
||||||
package: 'stream_chat_flutter',
|
package: 'stream_chat_flutter',
|
||||||
width: 24.0,
|
width: 24,
|
||||||
height: 24.0,
|
height: 24,
|
||||||
color: color,
|
color: color,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -193,11 +196,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
return _buildDisconnectedTitleState(context, _client);
|
return _buildDisconnectedTitleState(context, _client);
|
||||||
default:
|
default:
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
subtitle ?? Offstage(),
|
subtitle ?? const Offstage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -213,18 +216,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildConnectingTitleState(BuildContext context) {
|
Widget _buildConnectingTitleState(BuildContext context) => Row(
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 16,
|
width: 16,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'Searching for Network',
|
'Searching for Network',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
@@ -237,13 +239,12 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDisconnectedTitleState(
|
Widget _buildDisconnectedTitleState(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
StreamChatClient client,
|
StreamChatClient client,
|
||||||
) {
|
) =>
|
||||||
return Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -275,8 +276,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
import 'channel_bottom_sheet.dart';
|
|
||||||
import 'channel_preview.dart';
|
|
||||||
|
|
||||||
/// Callback called when tapping on a channel
|
/// Callback called when tapping on a channel
|
||||||
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
||||||
|
|
||||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||||
|
|
||||||
|
/// Callback for when 'View Info' is tapped
|
||||||
typedef ViewInfoCallback = void Function(Channel);
|
typedef ViewInfoCallback = void Function(Channel);
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
@@ -47,14 +46,16 @@ typedef ViewInfoCallback = void Function(Channel);
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
|
/// Make sure to have a [StreamChat] ancestor in order to provide the
|
||||||
|
/// information about the channels.
|
||||||
/// The widget uses a [ListView.custom] to render the list of channels.
|
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget components render the ui based on the first ancestor of
|
||||||
|
/// type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelListView extends StatefulWidget {
|
class ChannelListView extends StatefulWidget {
|
||||||
/// Instantiate a new ChannelListView
|
/// Instantiate a new ChannelListView
|
||||||
ChannelListView({
|
const ChannelListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.filter,
|
this.filter,
|
||||||
this.options,
|
this.options,
|
||||||
@@ -94,8 +95,10 @@ class ChannelListView extends StatefulWidget {
|
|||||||
final Map<String, dynamic>? options;
|
final Map<String, dynamic>? options;
|
||||||
|
|
||||||
/// The sorting used for the channels matching the filters.
|
/// The sorting used for the channels matching the filters.
|
||||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
/// Sorting is based on field and direction, multiple sorting options
|
||||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
/// can be provided.
|
||||||
|
/// You can sort based on last_updated, last_message_at, updated_at,
|
||||||
|
/// created_at or member_count.
|
||||||
/// Direction can be ascending or descending.
|
/// Direction can be ascending or descending.
|
||||||
final List<SortOption<ChannelModel>>? sort;
|
final List<SortOption<ChannelModel>>? sort;
|
||||||
|
|
||||||
@@ -137,8 +140,10 @@ class ChannelListView extends StatefulWidget {
|
|||||||
/// The amount of space by which to inset the children.
|
/// The amount of space by which to inset the children.
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// List of selected channels which are displayed differently
|
||||||
final List<Channel> selectedChannels;
|
final List<Channel> selectedChannels;
|
||||||
|
|
||||||
|
/// Callback for when 'View Info' is tapped
|
||||||
final ViewInfoCallback? onViewInfoTap;
|
final ViewInfoCallback? onViewInfoTap;
|
||||||
|
|
||||||
/// The builder that will be used in case of error
|
/// The builder that will be used in case of error
|
||||||
@@ -203,15 +208,14 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
crossAxisCount: widget.crossAxisCount,
|
crossAxisCount: widget.crossAxisCount,
|
||||||
),
|
),
|
||||||
itemCount: channels.length,
|
itemCount: channels.length,
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) =>
|
||||||
return _gridItemBuilder(context, index, channels);
|
_gridItemBuilder(context, index, channels),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = ListView.separated(
|
child = ListView.separated(
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount:
|
itemCount:
|
||||||
channels.isNotEmpty ? channels.length + 1 : channels.length,
|
channels.isNotEmpty ? channels.length + 1 : channels.length,
|
||||||
separatorBuilder: (_, index) {
|
separatorBuilder: (_, index) {
|
||||||
@@ -220,9 +224,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
}
|
}
|
||||||
return _separatorBuilder(context, index);
|
return _separatorBuilder(context, index);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) =>
|
||||||
return _listItemBuilder(context, index, channels);
|
_listItemBuilder(context, index, channels),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +236,9 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyWidget(BuildContext context) {
|
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
|
||||||
return LayoutBuilder(
|
builder: (context, viewportConstraints) => SingleChildScrollView(
|
||||||
builder: (context, viewportConstraints) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return SingleChildScrollView(
|
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
@@ -248,7 +249,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: StreamSvgIcon.message(
|
child: StreamSvgIcon.message(
|
||||||
size: 136,
|
size: 136,
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
@@ -257,7 +258,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Let’s start chatting!',
|
'Let’s start chatting!',
|
||||||
style: StreamChatTheme.of(context).textTheme.headline,
|
style: StreamChatTheme.of(context).textTheme.headline,
|
||||||
@@ -265,7 +266,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal: 52,
|
horizontal: 52,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -307,15 +308,12 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLoadingWidget(BuildContext context) {
|
Widget _buildLoadingWidget(BuildContext context) => ListView(
|
||||||
return ListView(
|
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
25,
|
25,
|
||||||
(i) {
|
(i) {
|
||||||
@@ -331,7 +329,6 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Shimmer _buildLoadingItem(BuildContext context) {
|
Shimmer _buildLoadingItem(BuildContext context) {
|
||||||
if (widget.crossAxisCount > 1) {
|
if (widget.crossAxisCount > 1) {
|
||||||
@@ -340,25 +337,25 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 4.0),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < widget.crossAxisCount; i++)
|
for (int i = 0; i < widget.crossAxisCount; i++)
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 70,
|
height: 70,
|
||||||
width: 70,
|
width: 70,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 16.0,
|
height: 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -373,7 +370,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
@@ -389,7 +386,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
borderRadius: BorderRadius.circular(11),
|
borderRadius: BorderRadius.circular(11),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 82,
|
width: 82,
|
||||||
),
|
),
|
||||||
@@ -406,7 +403,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
borderRadius: BorderRadius.circular(11),
|
borderRadius: BorderRadius.circular(11),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.expand(
|
constraints: const BoxConstraints.expand(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -418,7 +415,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
borderRadius: BorderRadius.circular(11),
|
borderRadius: BorderRadius.circular(11),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 42,
|
width: 42,
|
||||||
),
|
),
|
||||||
@@ -430,18 +427,17 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorWidget(BuildContext context, Object error) {
|
Widget _buildErrorWidget(BuildContext context, Object error) => Center(
|
||||||
return Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
const TextSpan(
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 2.0,
|
right: 2,
|
||||||
),
|
),
|
||||||
child: Icon(Icons.error_outline),
|
child: Icon(Icons.error_outline),
|
||||||
),
|
),
|
||||||
@@ -453,12 +449,11 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => _channelListController.loadData!(),
|
onPressed: () => _channelListController.loadData!(),
|
||||||
child: Text('Retry'),
|
child: const Text('Retry'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||||
final channelsProvider = ChannelsBloc.of(context);
|
final channelsProvider = ChannelsBloc.of(context);
|
||||||
@@ -471,13 +466,11 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) => Slidable(
|
||||||
return Slidable(
|
|
||||||
controller: _slideController,
|
controller: _slideController,
|
||||||
enabled: widget.swipeToAction,
|
enabled: widget.swipeToAction,
|
||||||
actionPane: SlidableBehindActionPane(),
|
actionPane: const SlidableBehindActionPane(),
|
||||||
actionExtentRatio: 0.12,
|
actionExtentRatio: 0.12,
|
||||||
closeOnScroll: true,
|
|
||||||
secondaryActions: <Widget>[
|
secondaryActions: <Widget>[
|
||||||
IconSlideAction(
|
IconSlideAction(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
@@ -485,23 +478,21 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(32),
|
topLeft: Radius.circular(32),
|
||||||
topRight: Radius.circular(32),
|
topRight: Radius.circular(32),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: ChannelBottomSheet(
|
child: ChannelBottomSheet(
|
||||||
onViewInfoTap: () {
|
onViewInfoTap: () {
|
||||||
widget.onViewInfoTap?.call(channel);
|
widget.onViewInfoTap?.call(channel);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -526,8 +517,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
'Are you sure you want to delete this conversation?',
|
'Are you sure you want to delete this conversation?',
|
||||||
cancelText: 'CANCEL',
|
cancelText: 'CANCEL',
|
||||||
icon: StreamSvgIcon.delete(
|
icon: StreamSvgIcon.delete(
|
||||||
color:
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
StreamChatTheme.of(context).colorTheme.accentRed,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (res == true) {
|
if (res == true) {
|
||||||
@@ -546,8 +536,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -567,12 +556,10 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: client,
|
channel: client,
|
||||||
child: widget.channelWidget!,
|
child: widget.channelWidget!,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -589,24 +576,23 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
ChannelImage(
|
ChannelImage(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
),
|
),
|
||||||
onTap: () => _getChannelTap(context),
|
onTap: () => _getChannelTap(context),
|
||||||
),
|
),
|
||||||
SizedBox(height: 7),
|
const SizedBox(height: 7),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: ChannelName(
|
child: const ChannelName(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -622,8 +608,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
Widget _buildQueryProgressIndicator(
|
Widget _buildQueryProgressIndicator(
|
||||||
context,
|
context,
|
||||||
ChannelsBlocState channelsProvider,
|
ChannelsBlocState channelsProvider,
|
||||||
) {
|
) =>
|
||||||
return StreamBuilder<bool>(
|
StreamBuilder<bool>(
|
||||||
stream: channelsProvider.queryChannelsLoading,
|
stream: channelsProvider.queryChannelsLoading,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@@ -633,8 +619,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.accentRed
|
.accentRed
|
||||||
.withOpacity(.2),
|
.withOpacity(.2),
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('Error loading channels'),
|
child: Text('Error loading channels'),
|
||||||
),
|
),
|
||||||
@@ -642,18 +628,17 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return snapshot.data!
|
return snapshot.data!
|
||||||
? Center(
|
? const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16),
|
||||||
child: const CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Offstage();
|
: const Offstage();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
Widget _separatorBuilder(context, i) {
|
Widget _separatorBuilder(context, i) {
|
||||||
var effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
|
|
||||||
/// It shows the current [Channel] name using a [Text] widget.
|
/// It shows the current [Channel] name using a [Text] widget.
|
||||||
///
|
///
|
||||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
/// The widget uses a [StreamBuilder] to render the channel information
|
||||||
|
/// image as soon as it updates.
|
||||||
class ChannelName extends StatelessWidget {
|
class ChannelName extends StatelessWidget {
|
||||||
/// Instantiate a new ChannelName
|
/// Instantiate a new ChannelName
|
||||||
const ChannelName({
|
const ChannelName({
|
||||||
@@ -25,9 +25,8 @@ class ChannelName extends StatelessWidget {
|
|||||||
return StreamBuilder<Map<String, dynamic>>(
|
return StreamBuilder<Map<String, dynamic>>(
|
||||||
stream: channel.extraDataStream,
|
stream: channel.extraDataStream,
|
||||||
initialData: channel.extraData,
|
initialData: channel.extraData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) =>
|
||||||
return _buildName(snapshot.data!, channel.state?.members, client);
|
_buildName(snapshot.data!, channel.state?.members, client),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +34,8 @@ class ChannelName extends StatelessWidget {
|
|||||||
Map<String, dynamic> extraData,
|
Map<String, dynamic> extraData,
|
||||||
List<Member>? members,
|
List<Member>? members,
|
||||||
StreamChatState client,
|
StreamChatState client,
|
||||||
) {
|
) =>
|
||||||
return LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
String? title;
|
String? title;
|
||||||
if (extraData['name'] == null) {
|
if (extraData['name'] == null) {
|
||||||
@@ -50,7 +49,8 @@ class ChannelName extends StatelessWidget {
|
|||||||
var currentChars = 0;
|
var currentChars = 0;
|
||||||
final currentMembers = <Member>[];
|
final currentMembers = <Member>[];
|
||||||
otherMembers!.forEach((element) {
|
otherMembers!.forEach((element) {
|
||||||
final newLength = currentChars + (element.user?.name.length ?? 0);
|
final newLength =
|
||||||
|
currentChars + (element.user?.name.length ?? 0);
|
||||||
if (newLength < maxChars) {
|
if (newLength < maxChars) {
|
||||||
currentChars = newLength;
|
currentChars = newLength;
|
||||||
currentMembers.add(element);
|
currentMembers.add(element);
|
||||||
@@ -59,8 +59,8 @@ class ChannelName extends StatelessWidget {
|
|||||||
|
|
||||||
final exceedingMembers =
|
final exceedingMembers =
|
||||||
otherMembers.length - currentMembers.length;
|
otherMembers.length - currentMembers.length;
|
||||||
title =
|
title = '${currentMembers.map((e) => e.user?.name).join(', ')} '
|
||||||
'${currentMembers.map((e) => e.user?.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
|
'${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
|
||||||
} else {
|
} else {
|
||||||
title = 'No title';
|
title = 'No title';
|
||||||
}
|
}
|
||||||
@@ -76,4 +76,3 @@ class ChannelName extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,23 +3,38 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
import 'channel_name.dart';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
/// It shows the current [Channel] preview.
|
/// It shows the current [Channel] preview.
|
||||||
///
|
///
|
||||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
/// The widget uses a [StreamBuilder] to render the channel information
|
||||||
|
/// image as soon as it updates.
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
|
/// Usually you don't use this widget as it's the default channel preview
|
||||||
|
/// used by [ChannelListView].
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget renders the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelPreview extends StatelessWidget {
|
class ChannelPreview extends StatelessWidget {
|
||||||
|
/// Constructor for creating [ChannelPreview]
|
||||||
|
const ChannelPreview({
|
||||||
|
required this.channel,
|
||||||
|
Key? key,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onImageTap,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.sendingIndicator,
|
||||||
|
this.trailing,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function called when tapping this widget
|
/// Function called when tapping this widget
|
||||||
final void Function(Channel)? onTap;
|
final void Function(Channel)? onTap;
|
||||||
|
|
||||||
@@ -38,36 +53,25 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
/// Widget rendering the subtitle
|
/// Widget rendering the subtitle
|
||||||
final Widget? subtitle;
|
final Widget? subtitle;
|
||||||
|
|
||||||
/// Widget rendering the leading element, by default it shows the [ChannelImage]
|
/// Widget rendering the leading element, by default
|
||||||
|
/// it shows the [ChannelImage]
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
|
||||||
/// Widget rendering the trailing element, by default it shows the last message date
|
/// Widget rendering the trailing element,
|
||||||
|
/// by default it shows the last message date
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
|
/// Widget rendering the sending indicator,
|
||||||
|
/// by default it uses the [SendingIndicator] widget
|
||||||
final Widget? sendingIndicator;
|
final Widget? sendingIndicator;
|
||||||
|
|
||||||
ChannelPreview({
|
|
||||||
required this.channel,
|
|
||||||
Key? key,
|
|
||||||
this.onTap,
|
|
||||||
this.onLongPress,
|
|
||||||
this.onImageTap,
|
|
||||||
this.title,
|
|
||||||
this.subtitle,
|
|
||||||
this.leading,
|
|
||||||
this.sendingIndicator,
|
|
||||||
this.trailing,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<bool>(
|
||||||
stream: channel.isMutedStream,
|
stream: channel.isMutedStream,
|
||||||
initialData: channel.isMuted,
|
initialData: channel.isMuted,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => Opacity(
|
||||||
return Opacity(
|
|
||||||
opacity: snapshot.data! ? 0.5 : 1,
|
opacity: snapshot.data! ? 0.5 : 1,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@@ -105,7 +109,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
snapshot.data!.isEmpty ||
|
snapshot.data!.isEmpty ||
|
||||||
!snapshot.data!.any((Member e) =>
|
!snapshot.data!.any((Member e) =>
|
||||||
e.user!.id == channel.client.state.user?.id)) {
|
e.user!.id == channel.client.state.user?.id)) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
return UnreadIndicator(
|
return UnreadIndicator(
|
||||||
cid: channel.cid,
|
cid: channel.cid,
|
||||||
@@ -128,7 +132,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
if (lastMessage?.user?.id ==
|
if (lastMessage?.user?.id ==
|
||||||
StreamChat.of(context).user?.id) {
|
StreamChat.of(context).user?.id) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: SendingIndicator(
|
child: SendingIndicator(
|
||||||
message: lastMessage!,
|
message: lastMessage!,
|
||||||
size: channelPreviewTheme.indicatorIconSize,
|
size: channelPreviewTheme.indicatorIconSize,
|
||||||
@@ -143,37 +147,37 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
trailing ?? _buildDate(context),
|
trailing ?? _buildDate(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDate(BuildContext context) {
|
Widget _buildDate(BuildContext context) => StreamBuilder<DateTime?>(
|
||||||
return StreamBuilder<DateTime?>(
|
|
||||||
stream: channel.lastMessageAtStream,
|
stream: channel.lastMessageAtStream,
|
||||||
initialData: channel.lastMessageAt,
|
initialData: channel.lastMessageAt,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
final lastMessageAt = snapshot.data!.toLocal();
|
final lastMessageAt = snapshot.data!.toLocal();
|
||||||
|
|
||||||
String stringDate;
|
String stringDate;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
var startOfDay = DateTime(now.year, now.month, now.day);
|
final startOfDay = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
if (lastMessageAt.millisecondsSinceEpoch >=
|
if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay.millisecondsSinceEpoch) {
|
startOfDay.millisecondsSinceEpoch) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
|
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
|
||||||
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
|
startOfDay
|
||||||
|
.subtract(const Duration(days: 1))
|
||||||
|
.millisecondsSinceEpoch) {
|
||||||
stringDate = 'Yesterday';
|
stringDate = 'Yesterday';
|
||||||
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
||||||
@@ -183,11 +187,11 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
stringDate,
|
stringDate,
|
||||||
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
|
style:
|
||||||
|
StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubtitle(BuildContext context) {
|
Widget _buildSubtitle(BuildContext context) {
|
||||||
if (channel.isMuted) {
|
if (channel.isMuted) {
|
||||||
@@ -211,15 +215,15 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLastMessage(BuildContext context) {
|
Widget _buildLastMessage(BuildContext context) =>
|
||||||
return StreamBuilder<List<Message>?>(
|
StreamBuilder<List<Message>?>(
|
||||||
stream: channel.state!.messagesStream,
|
stream: channel.state!.messagesStream,
|
||||||
initialData: channel.state!.messages,
|
initialData: channel.state!.messages,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final lastMessage = snapshot.data
|
final lastMessage = snapshot.data
|
||||||
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = lastMessage.text;
|
var text = lastMessage.text;
|
||||||
@@ -246,7 +250,10 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
text,
|
text,
|
||||||
lastMessage.mentionedUsers,
|
lastMessage.mentionedUsers,
|
||||||
lastMessage.attachments,
|
lastMessage.attachments,
|
||||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith(
|
StreamChatTheme.of(context)
|
||||||
|
.channelPreviewTheme
|
||||||
|
.subtitle
|
||||||
|
?.copyWith(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
.subtitle
|
.subtitle
|
||||||
@@ -254,7 +261,10 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||||
? FontStyle.italic
|
? FontStyle.italic
|
||||||
: FontStyle.normal),
|
: FontStyle.normal),
|
||||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith(
|
StreamChatTheme.of(context)
|
||||||
|
.channelPreviewTheme
|
||||||
|
.subtitle
|
||||||
|
?.copyWith(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
.subtitle
|
.subtitle
|
||||||
@@ -270,7 +280,6 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
TextSpan _getDisplayText(
|
TextSpan _getDisplayText(
|
||||||
String text,
|
String text,
|
||||||
@@ -298,7 +307,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
resList.add(TextSpan(
|
resList.add(TextSpan(
|
||||||
text: e == textList.last ? '$e' : '$e ',
|
text: e == textList.last ? e : '$e ',
|
||||||
style: normalTextStyle,
|
style: normalTextStyle,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
|
|
||||||
/// Widget that builds itself based on the latest snapshot of interaction with
|
/// Widget that builds itself based on the latest snapshot of interaction with
|
||||||
/// a [Stream] of type [ConnectionStatus].
|
/// a [Stream] of type [ConnectionStatus].
|
||||||
///
|
///
|
||||||
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] in case no
|
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
|
||||||
/// stream is provided.
|
/// in case no stream is provided.
|
||||||
class ConnectionStatusBuilder extends StatelessWidget {
|
class ConnectionStatusBuilder extends StatelessWidget {
|
||||||
/// Creates a new ConnectionStatusBuilder
|
/// Creates a new ConnectionStatusBuilder
|
||||||
const ConnectionStatusBuilder({
|
const ConnectionStatusBuilder({
|
||||||
@@ -47,11 +46,11 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
|||||||
if (errorBuilder != null) {
|
if (errorBuilder != null) {
|
||||||
return errorBuilder!(context, snapshot.error);
|
return errorBuilder!(context, snapshot.error);
|
||||||
}
|
}
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
if (loadingBuilder != null) return loadingBuilder!(context);
|
if (loadingBuilder != null) return loadingBuilder!(context);
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return statusBuilder(context, snapshot.data!);
|
return statusBuilder(context, snapshot.data!);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
|||||||
|
|
||||||
/// It shows a date divider depending on the date difference
|
/// It shows a date divider depending on the date difference
|
||||||
class DateDivider extends StatelessWidget {
|
class DateDivider extends StatelessWidget {
|
||||||
final DateTime dateTime;
|
/// Constructor for creating a [DateDivider]
|
||||||
final bool uppercase;
|
|
||||||
|
|
||||||
const DateDivider({
|
const DateDivider({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.dateTime,
|
required this.dateTime,
|
||||||
this.uppercase = false,
|
this.uppercase = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// [DateTime] to display
|
||||||
|
final DateTime dateTime;
|
||||||
|
|
||||||
|
/// If text is uppercase
|
||||||
|
final bool uppercase;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final createdAt = Jiffy(dateTime);
|
final createdAt = Jiffy(dateTime);
|
||||||
@@ -22,10 +26,10 @@ class DateDivider extends StatelessWidget {
|
|||||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||||
dayInfo = 'Today';
|
dayInfo = 'Today';
|
||||||
} else if (Jiffy(createdAt)
|
} else if (Jiffy(createdAt)
|
||||||
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
|
.isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
|
||||||
dayInfo = 'Yesterday';
|
dayInfo = 'Yesterday';
|
||||||
} else if (Jiffy(createdAt).isAfter(
|
} else if (Jiffy(createdAt).isAfter(
|
||||||
now.subtract(Duration(days: 7)),
|
now.subtract(const Duration(days: 7)),
|
||||||
Units.DAY,
|
Units.DAY,
|
||||||
)) {
|
)) {
|
||||||
dayInfo = createdAt.EEEE;
|
dayInfo = createdAt.EEEE;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
|
||||||
|
/// Widget to display deleted message
|
||||||
class DeletedMessage extends StatelessWidget {
|
class DeletedMessage extends StatelessWidget {
|
||||||
|
/// Constructor to create [DeletedMessage]
|
||||||
const DeletedMessage({
|
const DeletedMessage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.messageTheme,
|
required this.messageTheme,
|
||||||
@@ -29,8 +31,7 @@ class DeletedMessage extends StatelessWidget {
|
|||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Transform(
|
||||||
return Transform(
|
|
||||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Material(
|
child: Material(
|
||||||
@@ -53,7 +54,7 @@ class DeletedMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
child: Transform(
|
child: Transform(
|
||||||
@@ -71,4 +72,3 @@ class DeletedMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ final _emojis = Emoji.all();
|
|||||||
/// String extension
|
/// String extension
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
/// Returns the capitalized string
|
/// Returns the capitalized string
|
||||||
String capitalize() {
|
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
|
||||||
return '${this[0].toUpperCase()}${substring(1)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the string contains only emoji's or not.
|
/// Returns whether the string contains only emoji's or not.
|
||||||
///
|
///
|
||||||
@@ -46,7 +44,9 @@ extension PlatformFileX on PlatformFile {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
extension InputDecorationX on InputDecoration {
|
extension InputDecorationX on InputDecoration {
|
||||||
|
///
|
||||||
InputDecoration merge(InputDecoration? other) {
|
InputDecoration merge(InputDecoration? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -98,7 +98,9 @@ extension InputDecorationX on InputDecoration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets text scale factor through context
|
||||||
extension BuildContextX on BuildContext {
|
extension BuildContextX on BuildContext {
|
||||||
|
// ignore: public_member_api_docs
|
||||||
double get textScaleFactor =>
|
double get textScaleFactor =>
|
||||||
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0;
|
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,24 +8,23 @@ import 'package:jiffy/jiffy.dart';
|
|||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/image_footer.dart';
|
import 'package:stream_chat_flutter/src/image_footer.dart';
|
||||||
import 'package:stream_chat_flutter/src/image_header.dart';
|
import 'package:stream_chat_flutter/src/image_header.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Return action for coming back from pages
|
||||||
|
enum ReturnActionType {
|
||||||
|
/// No return action
|
||||||
|
none,
|
||||||
|
|
||||||
enum ReturnActionType { none, reply }
|
/// Go to reply message action
|
||||||
|
reply,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
typedef ShowMessageCallback = void Function(Message message, Channel channel);
|
typedef ShowMessageCallback = void Function(Message message, Channel channel);
|
||||||
|
|
||||||
/// A full screen image widget
|
/// A full screen image widget
|
||||||
class FullScreenMedia extends StatefulWidget {
|
class FullScreenMedia extends StatefulWidget {
|
||||||
/// The url of the image
|
|
||||||
final List<Attachment> mediaAttachments;
|
|
||||||
final Message message;
|
|
||||||
|
|
||||||
final int startIndex;
|
|
||||||
final String userName;
|
|
||||||
final ShowMessageCallback? onShowMessage;
|
|
||||||
|
|
||||||
/// Instantiate a new FullScreenImage
|
/// Instantiate a new FullScreenImage
|
||||||
const FullScreenMedia({
|
const FullScreenMedia({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -37,6 +36,21 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
}) : userName = userName ?? '',
|
}) : userName = userName ?? '',
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
/// The url of the image
|
||||||
|
final List<Attachment> mediaAttachments;
|
||||||
|
|
||||||
|
/// Message where attachments are attached
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// First index of media shown
|
||||||
|
final int startIndex;
|
||||||
|
|
||||||
|
/// Username of sender
|
||||||
|
final String userName;
|
||||||
|
|
||||||
|
/// Callback for when show message is tapped
|
||||||
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||||
}
|
}
|
||||||
@@ -57,7 +71,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
_pageController = PageController(initialPage: widget.startIndex);
|
_pageController = PageController(initialPage: widget.startIndex);
|
||||||
_currentPage = widget.startIndex;
|
_currentPage = widget.startIndex;
|
||||||
@@ -77,15 +91,13 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Scaffold(
|
||||||
return Scaffold(
|
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => PageView.builder(
|
||||||
return PageView.builder(
|
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
onPageChanged: (val) {
|
onPageChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -133,7 +145,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
} else if (attachment.type == 'video') {
|
} else if (attachment.type == 'video') {
|
||||||
final controller = videoPackages[attachment.id]!;
|
final controller = videoPackages[attachment.id]!;
|
||||||
if (!controller.initialized) {
|
if (!controller.initialized) {
|
||||||
return Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,7 +162,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 50.0,
|
vertical: 50,
|
||||||
),
|
),
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: controller.chewieController!,
|
controller: controller.chewieController!,
|
||||||
@@ -161,17 +173,17 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
return Container();
|
return Container();
|
||||||
},
|
},
|
||||||
itemCount: widget.mediaAttachments.length,
|
itemCount: widget.mediaAttachments.length,
|
||||||
);
|
)),
|
||||||
}),
|
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: _optionsShown ? 1.0 : 0.0,
|
opacity: _optionsShown ? 1.0 : 0.0,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ImageHeader(
|
ImageHeader(
|
||||||
userName: widget.userName,
|
userName: widget.userName,
|
||||||
sentAt:
|
sentAt:
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}',
|
'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}',
|
||||||
onBackPressed: () {
|
onBackPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -196,7 +208,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
_currentPage = val;
|
_currentPage = val;
|
||||||
_pageController.animateToPage(
|
_pageController.animateToPage(
|
||||||
val,
|
val,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -209,7 +221,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
String getDay(DateTime dateTime) {
|
String getDay(DateTime dateTime) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -236,18 +247,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for packaging up things required for videos
|
||||||
class VideoPackage {
|
class VideoPackage {
|
||||||
final bool _showControls;
|
/// Constructor for creating [VideoPackage]
|
||||||
final bool _autoInitialize;
|
|
||||||
final VideoPlayerController _videoPlayerController;
|
|
||||||
ChewieController? _chewieController;
|
|
||||||
|
|
||||||
VideoPlayerController get videoPlayer => _videoPlayerController;
|
|
||||||
|
|
||||||
ChewieController? get chewieController => _chewieController;
|
|
||||||
|
|
||||||
bool get initialized => _videoPlayerController.value.isInitialized;
|
|
||||||
|
|
||||||
VideoPackage(
|
VideoPackage(
|
||||||
Attachment attachment, {
|
Attachment attachment, {
|
||||||
bool showControls = false,
|
bool showControls = false,
|
||||||
@@ -258,8 +260,22 @@ class VideoPackage {
|
|||||||
? VideoPlayerController.file(File.fromUri(attachment.localUri!))
|
? VideoPlayerController.file(File.fromUri(attachment.localUri!))
|
||||||
: VideoPlayerController.network(attachment.assetUrl!);
|
: VideoPlayerController.network(attachment.assetUrl!);
|
||||||
|
|
||||||
Future<void> initialize() {
|
final bool _showControls;
|
||||||
return _videoPlayerController.initialize().then((_) {
|
final bool _autoInitialize;
|
||||||
|
final VideoPlayerController _videoPlayerController;
|
||||||
|
ChewieController? _chewieController;
|
||||||
|
|
||||||
|
/// Get video player for video
|
||||||
|
VideoPlayerController get videoPlayer => _videoPlayerController;
|
||||||
|
|
||||||
|
/// Get [ChewieController] for video
|
||||||
|
ChewieController? get chewieController => _chewieController;
|
||||||
|
|
||||||
|
/// Check if controller is initialised
|
||||||
|
bool get initialized => _videoPlayerController.value.isInitialized;
|
||||||
|
|
||||||
|
/// Initialize all things required for [VideoPackage]
|
||||||
|
Future<void> initialize() => _videoPlayerController.initialize().then((_) {
|
||||||
_chewieController = ChewieController(
|
_chewieController = ChewieController(
|
||||||
videoPlayerController: _videoPlayerController,
|
videoPlayerController: _videoPlayerController,
|
||||||
autoInitialize: _autoInitialize,
|
autoInitialize: _autoInitialize,
|
||||||
@@ -267,16 +283,16 @@ class VideoPackage {
|
|||||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
void addListener(VoidCallback listener) {
|
/// Add a listener to video player controller
|
||||||
return _videoPlayerController.addListener(listener);
|
void addListener(VoidCallback listener) =>
|
||||||
}
|
_videoPlayerController.addListener(listener);
|
||||||
|
|
||||||
void removeListener(VoidCallback listener) {
|
/// Remove a listener to video player controller
|
||||||
return _videoPlayerController.removeListener(listener);
|
void removeListener(VoidCallback listener) =>
|
||||||
}
|
_videoPlayerController.removeListener(listener);
|
||||||
|
|
||||||
|
/// Dispose controllers
|
||||||
Future<void> dispose() {
|
Future<void> dispose() {
|
||||||
_chewieController?.dispose();
|
_chewieController?.dispose();
|
||||||
return _videoPlayerController.dispose();
|
return _videoPlayerController.dispose();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Widget for constructing a group of images
|
||||||
|
|
||||||
class GroupImage extends StatelessWidget {
|
class GroupImage extends StatelessWidget {
|
||||||
|
/// Constructor for creating a [GroupImage]
|
||||||
const GroupImage({
|
const GroupImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.images,
|
required this.images,
|
||||||
@@ -15,17 +16,30 @@ class GroupImage extends StatelessWidget {
|
|||||||
this.selectionThickness = 4,
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// List of images to display
|
||||||
final List<String> images;
|
final List<String> images;
|
||||||
|
|
||||||
|
/// Constraints on the widget
|
||||||
final BoxConstraints? constraints;
|
final BoxConstraints? constraints;
|
||||||
|
|
||||||
|
/// Callback when widget is tapped
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
/// Highlights if selected
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
|
||||||
|
/// [BorderRadius] to pass to the widget
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
|
/// Color of selection if selected
|
||||||
final Color? selectionColor;
|
final Color? selectionColor;
|
||||||
|
|
||||||
|
/// Thickness with which color of selection is shown
|
||||||
final double selectionThickness;
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var avatar;
|
Widget? avatar;
|
||||||
final streamChatTheme = StreamChatTheme.of(context);
|
final streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
avatar = GestureDetector(
|
avatar = GestureDetector(
|
||||||
@@ -113,8 +127,8 @@ class GroupImage extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
color: selectionColor ??
|
color: selectionColor ??
|
||||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
height: 64.0,
|
height: 64,
|
||||||
width: 64.0,
|
width: 64,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(selectionThickness),
|
padding: EdgeInsets.all(selectionThickness),
|
||||||
child: avatar,
|
child: avatar,
|
||||||
|
|||||||
@@ -12,7 +12,22 @@ import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// Footer widget for media display
|
||||||
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||||
|
/// Creates a channel header
|
||||||
|
const ImageFooter({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
this.onBackPressed,
|
||||||
|
this.onTitleTap,
|
||||||
|
this.onImageTap,
|
||||||
|
this.currentPage = 0,
|
||||||
|
this.totalPages = 0,
|
||||||
|
this.mediaAttachments = const [],
|
||||||
|
this.mediaSelectedCallBack,
|
||||||
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// Callback to call when pressing the back button.
|
/// Callback to call when pressing the back button.
|
||||||
/// By default it calls [Navigator.pop]
|
/// By default it calls [Navigator.pop]
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -23,28 +38,21 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
/// Callback to call when the image is tapped.
|
/// Callback to call when the image is tapped.
|
||||||
final VoidCallback? onImageTap;
|
final VoidCallback? onImageTap;
|
||||||
|
|
||||||
|
/// Stores the current index of media shown
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
|
|
||||||
|
/// Total number of pages of media
|
||||||
final int totalPages;
|
final int totalPages;
|
||||||
|
|
||||||
|
/// All attachments to show
|
||||||
final List<Attachment> mediaAttachments;
|
final List<Attachment> mediaAttachments;
|
||||||
|
|
||||||
|
/// Message which attachments are attached to
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// Callback when media is selected
|
||||||
final ValueChanged<int>? mediaSelectedCallBack;
|
final ValueChanged<int>? mediaSelectedCallBack;
|
||||||
|
|
||||||
/// Creates a channel header
|
|
||||||
ImageFooter({
|
|
||||||
Key? key,
|
|
||||||
required this.message,
|
|
||||||
this.onBackPressed,
|
|
||||||
this.onTitleTap,
|
|
||||||
this.onImageTap,
|
|
||||||
this.currentPage = 0,
|
|
||||||
this.totalPages = 0,
|
|
||||||
this.mediaAttachments = const [],
|
|
||||||
this.mediaSelectedCallBack,
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ImageFooterState createState() => _ImageFooterState();
|
_ImageFooterState createState() => _ImageFooterState();
|
||||||
|
|
||||||
@@ -82,13 +90,14 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
!showShareButton
|
if (!showShareButton)
|
||||||
? Container(
|
Container(
|
||||||
width: 48,
|
width: 48,
|
||||||
)
|
)
|
||||||
: IconButton(
|
else
|
||||||
|
IconButton(
|
||||||
icon: StreamSvgIcon.iconShare(
|
icon: StreamSvgIcon.iconShare(
|
||||||
size: 24.0,
|
size: 24,
|
||||||
color: StreamChatTheme.of(context).colorTheme.black,
|
color: StreamChatTheme.of(context).colorTheme.black,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -100,14 +109,12 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
final type = attachment.type == 'image'
|
final type = attachment.type == 'image'
|
||||||
? 'jpg'
|
? 'jpg'
|
||||||
: url.split('?').first.split('.').last;
|
: url.split('?').first.split('.').last;
|
||||||
final request =
|
final request = await HttpClient().getUrl(Uri.parse(url));
|
||||||
await HttpClient().getUrl(Uri.parse(url));
|
|
||||||
final response = await request.close();
|
final response = await request.close();
|
||||||
final bytes =
|
final bytes =
|
||||||
await consolidateHttpClientResponseBytes(response);
|
await consolidateHttpClientResponseBytes(response);
|
||||||
final tmpPath = await getTemporaryDirectory();
|
final tmpPath = await getTemporaryDirectory();
|
||||||
final filePath =
|
final filePath = '${tmpPath.path}/${attachment.id}.$type';
|
||||||
'${tmpPath.path}/${attachment.id}.$type';
|
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
await file.writeAsBytes(bytes);
|
await file.writeAsBytes(bytes);
|
||||||
await Share.shareFiles(
|
await Share.shareFiles(
|
||||||
@@ -120,9 +127,8 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: widget.onTitleTap,
|
onTap: widget.onTitleTap,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -156,12 +162,12 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16.0),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final crossAxisCount = 3;
|
const crossAxisCount = 3;
|
||||||
final noOfRowToShowInitially =
|
final noOfRowToShowInitially =
|
||||||
widget.mediaAttachments.length > crossAxisCount ? 2 : 1;
|
widget.mediaAttachments.length > crossAxisCount ? 2 : 1;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -171,8 +177,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
expand: false,
|
expand: false,
|
||||||
initialChildSize: initialChildSize / size.height,
|
initialChildSize: initialChildSize / size.height,
|
||||||
minChildSize: initialChildSize / size.height,
|
minChildSize: initialChildSize / size.height,
|
||||||
builder: (context, scrollController) {
|
builder: (context, scrollController) => SingleChildScrollView(
|
||||||
return SingleChildScrollView(
|
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -181,7 +186,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Photos',
|
'Photos',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
@@ -207,10 +212,12 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: widget.mediaAttachments.length,
|
itemCount: widget.mediaAttachments.length,
|
||||||
padding: const EdgeInsets.all(1),
|
padding: const EdgeInsets.all(1),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
// ignore: lines_longer_than_80_chars
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
mainAxisSpacing: 2.0,
|
mainAxisSpacing: 2,
|
||||||
crossAxisSpacing: 2.0,
|
crossAxisSpacing: 2,
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Widget media;
|
Widget media;
|
||||||
@@ -230,7 +237,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
media = InkWell(
|
media = InkWell(
|
||||||
onTap: () => widget.mediaSelectedCallBack!(index),
|
onTap: () => widget.mediaSelectedCallBack!(index),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1.0,
|
aspectRatio: 1,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: attachment.imageUrl ??
|
imageUrl: attachment.imageUrl ??
|
||||||
attachment.assetUrl ??
|
attachment.assetUrl ??
|
||||||
@@ -246,7 +253,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
media,
|
media,
|
||||||
if (widget.message.user != null)
|
if (widget.message.user != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Container(
|
child: Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -254,7 +261,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
color: Colors.white.withOpacity(0.6),
|
color: Colors.white.withOpacity(0.6),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 8.0,
|
blurRadius: 8,
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
@@ -266,7 +273,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
child: UserAvatar(
|
child: UserAvatar(
|
||||||
user: widget.message.user!,
|
user: widget.message.user!,
|
||||||
constraints:
|
constraints:
|
||||||
BoxConstraints.tight(Size(24, 24)),
|
BoxConstraints.tight(const Size(24, 24)),
|
||||||
showOnlineStatus: false,
|
showOnlineStatus: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -278,8 +285,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -287,7 +293,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
|
|
||||||
/// Sends the current message
|
/// Sends the current message
|
||||||
Future sendMessage() async {
|
Future sendMessage() async {
|
||||||
var text = _messageController.text.trim();
|
final text = _messageController.text.trim();
|
||||||
|
|
||||||
final attachments = widget.message.attachments;
|
final attachments = widget.message.attachments;
|
||||||
|
|
||||||
@@ -306,29 +312,3 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for clipping textfield prefix icon
|
|
||||||
class IconClipper extends CustomClipper<Path> {
|
|
||||||
@override
|
|
||||||
Path getClip(Size size) {
|
|
||||||
var leftX = size.width / 5;
|
|
||||||
var rightX = 4 * size.width / 5;
|
|
||||||
var topY = size.height / 5;
|
|
||||||
var bottomY = 4 * size.height / 5;
|
|
||||||
|
|
||||||
final path = Path();
|
|
||||||
path.moveTo(leftX, topY);
|
|
||||||
path.lineTo(leftX, bottomY);
|
|
||||||
path.lineTo(rightX, bottomY);
|
|
||||||
path.lineTo(rightX, topY);
|
|
||||||
path.lineTo(leftX, topY);
|
|
||||||
path.lineTo(0.0, 0.0);
|
|
||||||
path.close();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReclip(CustomClipper oldClipper) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// Widget for constructing a group of images in message
|
||||||
class ImageGroup extends StatelessWidget {
|
class ImageGroup extends StatelessWidget {
|
||||||
|
/// Constructor for creating [ImageGroup] widget
|
||||||
const ImageGroup({
|
const ImageGroup({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.images,
|
required this.images,
|
||||||
@@ -13,36 +15,41 @@ class ImageGroup extends StatelessWidget {
|
|||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// List of attachments to show
|
||||||
final List<Attachment> images;
|
final List<Attachment> images;
|
||||||
|
|
||||||
|
/// Message which images are attached to
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// [MessageTheme] to apply to message
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Size of iamges
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
|
/// Callback for when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => ConstrainedBox(
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints.loose(size),
|
constraints: BoxConstraints.loose(size),
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.vertical,
|
direction: Axis.vertical,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
child: Flex(
|
child: Flex(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
child: _buildImage(context, 0),
|
child: _buildImage(context, 0),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 2.0),
|
padding: const EdgeInsets.only(left: 2),
|
||||||
child: _buildImage(context, 1),
|
child: _buildImage(context, 1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -52,24 +59,21 @@ class ImageGroup extends StatelessWidget {
|
|||||||
if (images.length >= 3)
|
if (images.length >= 3)
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
flex: 1,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
flex: 1,
|
|
||||||
child: _buildImage(context, 2),
|
child: _buildImage(context, 2),
|
||||||
),
|
),
|
||||||
if (images.length >= 4)
|
if (images.length >= 4)
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
flex: 1,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 2.0),
|
padding: const EdgeInsets.only(left: 2),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -83,7 +87,7 @@ class ImageGroup extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'+ ${images.length - 4}',
|
'+ ${images.length - 4}',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
),
|
),
|
||||||
@@ -103,7 +107,6 @@ class ImageGroup extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void _onTap(
|
void _onTap(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -128,8 +131,7 @@ class ImageGroup extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(BuildContext context, int index) {
|
Widget _buildImage(BuildContext context, int index) => ImageAttachment(
|
||||||
return ImageAttachment(
|
|
||||||
attachment: images[index],
|
attachment: images[index],
|
||||||
size: size,
|
size: size,
|
||||||
message: message,
|
message: message,
|
||||||
@@ -137,4 +139,3 @@ class ImageGroup extends StatelessWidget {
|
|||||||
onAttachmentTap: () => _onTap(context, index),
|
onAttachmentTap: () => _onTap(context, index),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment_actions_modal.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'attachment_actions_modal.dart';
|
/// Header/AppBar widget for media display screen
|
||||||
|
|
||||||
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
/// Creates a channel header
|
||||||
|
const ImageHeader({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
this.currentIndex = 0,
|
||||||
|
this.showBackButton = true,
|
||||||
|
this.onBackPressed,
|
||||||
|
this.onShowMessage,
|
||||||
|
this.onTitleTap,
|
||||||
|
this.onImageTap,
|
||||||
|
this.userName = '',
|
||||||
|
this.sentAt = '',
|
||||||
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// True if this header shows the leading back button
|
/// True if this header shows the leading back button
|
||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
|
|
||||||
@@ -22,42 +37,31 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// Callback to call when the image is tapped.
|
/// Callback to call when the image is tapped.
|
||||||
final VoidCallback? onImageTap;
|
final VoidCallback? onImageTap;
|
||||||
|
|
||||||
|
/// Message which attachments are attached to
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// Username of sender
|
||||||
final String userName;
|
final String userName;
|
||||||
|
|
||||||
|
/// Text which connotes the time the message was sent
|
||||||
final String sentAt;
|
final String sentAt;
|
||||||
|
|
||||||
|
/// Stores the current index of media shown
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
|
|
||||||
/// Creates a channel header
|
|
||||||
ImageHeader({
|
|
||||||
Key? key,
|
|
||||||
required this.message,
|
|
||||||
this.currentIndex = 0,
|
|
||||||
this.showBackButton = true,
|
|
||||||
this.onBackPressed,
|
|
||||||
this.onShowMessage,
|
|
||||||
this.onTitleTap,
|
|
||||||
this.onImageTap,
|
|
||||||
this.userName = '',
|
|
||||||
this.sentAt = '',
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => AppBar(
|
||||||
return AppBar(
|
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: showBackButton
|
leading: showBackButton
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: StreamSvgIcon.close(
|
icon: StreamSvgIcon.close(
|
||||||
color: StreamChatTheme.of(context).colorTheme.black,
|
color: StreamChatTheme.of(context).colorTheme.black,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: const SizedBox(),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@@ -75,17 +79,17 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
title: message.type != 'ephemeral'
|
title: message.type != 'ephemeral'
|
||||||
? InkWell(
|
? InkWell(
|
||||||
onTap: onTitleTap,
|
onTap: onTitleTap,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: preferredSize.height,
|
height: preferredSize.height,
|
||||||
width: preferredSize.width,
|
width: preferredSize.width,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
userName,
|
userName,
|
||||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
style:
|
||||||
|
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
sentAt,
|
sentAt,
|
||||||
@@ -97,9 +101,8 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: const SizedBox(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Size preferredSize;
|
final Size preferredSize;
|
||||||
@@ -110,16 +113,14 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final result = await showDialog(
|
final result = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
message: message,
|
message: message,
|
||||||
currentIndex: currentIndex,
|
currentIndex: currentIndex,
|
||||||
onShowMessage: onShowMessage,
|
onShowMessage: onShowMessage,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_portal/flutter_portal.dart';
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
|
||||||
|
/// Tile to display a message, used in stream chat to display connection status
|
||||||
class InfoTile extends StatelessWidget {
|
class InfoTile extends StatelessWidget {
|
||||||
final String message;
|
/// Constructor for creating an [InfoTile] widget
|
||||||
final Widget child;
|
const InfoTile({
|
||||||
final bool showMessage;
|
Key? key,
|
||||||
final Alignment? tileAnchor;
|
|
||||||
final Alignment? childAnchor;
|
|
||||||
final TextStyle? textStyle;
|
|
||||||
final Color? backgroundColor;
|
|
||||||
|
|
||||||
InfoTile({
|
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.showMessage,
|
required this.showMessage,
|
||||||
@@ -19,16 +14,36 @@ class InfoTile extends StatelessWidget {
|
|||||||
this.childAnchor,
|
this.childAnchor,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// String to display
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Widget to display over
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Flag to show message
|
||||||
|
final bool showMessage;
|
||||||
|
|
||||||
|
/// Anchor for tile - [portalAnchor] for [PortalEntry]
|
||||||
|
final Alignment? tileAnchor;
|
||||||
|
|
||||||
|
/// Alignment for child - [childAnchor] for [PortalEntry]
|
||||||
|
final Alignment? childAnchor;
|
||||||
|
|
||||||
|
/// [TextStyle] for message
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
/// Background color for tile
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => PortalEntry(
|
||||||
return PortalEntry(
|
|
||||||
visible: showMessage,
|
visible: showMessage,
|
||||||
portalAnchor: tileAnchor ?? Alignment.topCenter,
|
portalAnchor: tileAnchor ?? Alignment.topCenter,
|
||||||
childAnchor: childAnchor ?? Alignment.bottomCenter,
|
childAnchor: childAnchor ?? Alignment.bottomCenter,
|
||||||
portal: Container(
|
portal: Container(
|
||||||
height: 25.0,
|
height: 25,
|
||||||
color: backgroundColor ??
|
color: backgroundColor ??
|
||||||
StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9),
|
StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -46,4 +61,3 @@ class InfoTile extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
|
|
||||||
extension on Duration {
|
extension on Duration {
|
||||||
String format() {
|
String format() {
|
||||||
@@ -19,16 +18,21 @@ extension on Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs a list of media
|
||||||
class MediaListView extends StatefulWidget {
|
class MediaListView extends StatefulWidget {
|
||||||
final List<String> selectedIds;
|
/// Constructor for creating a [MediaListView] widget
|
||||||
final void Function(AssetEntity media)? onSelect;
|
|
||||||
|
|
||||||
const MediaListView({
|
const MediaListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.selectedIds = const [],
|
this.selectedIds = const [],
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Stores the media selected
|
||||||
|
final List<String> selectedIds;
|
||||||
|
|
||||||
|
/// Callback for on media selected
|
||||||
|
final void Function(AssetEntity media)? onSelect;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MediaListViewState createState() => _MediaListViewState();
|
_MediaListViewState createState() => _MediaListViewState();
|
||||||
}
|
}
|
||||||
@@ -39,13 +43,12 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => LazyLoadScrollView(
|
||||||
return LazyLoadScrollView(
|
|
||||||
onEndOfPage: () async => _getMedia(),
|
onEndOfPage: () async => _getMedia(),
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
itemCount: _media.length,
|
itemCount: _media.length,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 3,
|
||||||
),
|
),
|
||||||
itemBuilder: (
|
itemBuilder: (
|
||||||
@@ -54,7 +57,7 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
) {
|
) {
|
||||||
final media = _media.elementAt(position);
|
final media = _media.elementAt(position);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
|
padding: const EdgeInsets.symmetric(horizontal: 1, vertical: 1),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (widget.onSelect != null) {
|
if (widget.onSelect != null) {
|
||||||
@@ -64,10 +67,10 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1.0,
|
aspectRatio: 1,
|
||||||
child: FadeInImage(
|
child: FadeInImage(
|
||||||
fadeInDuration: Duration(milliseconds: 300),
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
placeholder: AssetImage(
|
placeholder: const AssetImage(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
package: 'stream_chat_flutter',
|
package: 'stream_chat_flutter',
|
||||||
),
|
),
|
||||||
@@ -80,8 +83,9 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
opacity: widget.selectedIds.any((id) => id == media.id)
|
opacity:
|
||||||
|
widget.selectedIds.any((id) => id == media.id)
|
||||||
? 1.0
|
? 1.0
|
||||||
: 0.0,
|
: 0.0,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -100,8 +104,9 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
StreamChatTheme.of(context).colorTheme.white,
|
StreamChatTheme.of(context).colorTheme.white,
|
||||||
child: StreamSvgIcon.check(
|
child: StreamSvgIcon.check(
|
||||||
size: 24,
|
size: 24,
|
||||||
color:
|
color: StreamChatTheme.of(context)
|
||||||
StreamChatTheme.of(context).colorTheme.black,
|
.colorTheme
|
||||||
|
.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -135,7 +140,6 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -144,9 +148,7 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _getMedia() async {
|
void _getMedia() async {
|
||||||
final assetList = await PhotoManager.getAssetPathList(
|
final assetList = await PhotoManager.getAssetPathList().then((value) {
|
||||||
hasAll: true,
|
|
||||||
).then((value) {
|
|
||||||
if (value.isNotEmpty == true) {
|
if (value.isNotEmpty == true) {
|
||||||
return value.singleWhere((element) => element.isAll);
|
return value.singleWhere((element) => element.isAll);
|
||||||
}
|
}
|
||||||
@@ -167,36 +169,38 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ImageProvider implementation
|
||||||
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||||
|
/// Constructor for creating a [MediaThumbnailProvider]
|
||||||
const MediaThumbnailProvider({
|
const MediaThumbnailProvider({
|
||||||
required this.media,
|
required this.media,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Media to load
|
||||||
final AssetEntity media;
|
final AssetEntity media;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter load(key, decode) {
|
ImageStreamCompleter load(
|
||||||
return MultiFrameImageStreamCompleter(
|
MediaThumbnailProvider key, DecoderCallback decode) =>
|
||||||
|
MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: 1.0,
|
scale: 1,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('Id: ${media.id}');
|
yield ErrorDescription('Id: ${media.id}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Future<ui.Codec> _loadAsync(
|
Future<ui.Codec> _loadAsync(
|
||||||
MediaThumbnailProvider key, DecoderCallback decode) async {
|
MediaThumbnailProvider key, DecoderCallback decode) async {
|
||||||
assert(key == this);
|
assert(key == this, 'Checks MediaThumbnailProvider');
|
||||||
final bytes = await media.thumbData;
|
final bytes = await media.thumbData;
|
||||||
|
|
||||||
return await decode(bytes!);
|
return decode(bytes!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
|
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) =>
|
||||||
return SynchronousFuture<MediaThumbnailProvider>(this);
|
SynchronousFuture<MediaThumbnailProvider>(this);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// This widget is used for showing user tiles for mentions
|
/// This widget is used for showing user tiles for mentions
|
||||||
/// Use [title], [subtitle], [leading], [trailing] for substituting widgets in respective positions
|
/// Use [title], [subtitle], [leading], [trailing] for
|
||||||
|
/// substituting widgets in respective positions
|
||||||
class MentionTile extends StatelessWidget {
|
class MentionTile extends StatelessWidget {
|
||||||
|
/// Constructor for creating a [MentionTile] widget
|
||||||
|
const MentionTile(
|
||||||
|
this.member, {
|
||||||
|
Key? key,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Member to display in the tile
|
/// Member to display in the tile
|
||||||
final Member member;
|
final Member member;
|
||||||
|
|
||||||
@@ -20,36 +31,26 @@ class MentionTile extends StatelessWidget {
|
|||||||
/// Widget at the end of tile
|
/// Widget at the end of tile
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
MentionTile(
|
|
||||||
this.member, {
|
|
||||||
this.title,
|
|
||||||
this.subtitle,
|
|
||||||
this.leading,
|
|
||||||
this.trailing,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SizedBox(
|
||||||
return Container(
|
height: 56,
|
||||||
height: 56.0,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 16.0,
|
width: 16,
|
||||||
),
|
),
|
||||||
leading ??
|
leading ??
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
constraints: BoxConstraints.tight(
|
constraints: BoxConstraints.tight(
|
||||||
Size(
|
const Size(
|
||||||
40,
|
40,
|
||||||
40,
|
40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
user: member.user!,
|
user: member.user!,
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 8.0,
|
width: 8,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -60,13 +61,13 @@ class MentionTile extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
title ??
|
title ??
|
||||||
Text(
|
Text(
|
||||||
'${member.user!.name}',
|
member.user!.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 2.0,
|
height: 2,
|
||||||
),
|
),
|
||||||
subtitle ??
|
subtitle ??
|
||||||
Text(
|
Text(
|
||||||
@@ -88,8 +89,8 @@ class MentionTile extends StatelessWidget {
|
|||||||
trailing ??
|
trailing ??
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: 18.0,
|
right: 18,
|
||||||
left: 8.0,
|
left: 8,
|
||||||
),
|
),
|
||||||
child: StreamSvgIcon.mentions(
|
child: StreamSvgIcon.mentions(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
@@ -99,4 +100,3 @@ class MentionTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
|
|
||||||
/// Class describing a message action
|
/// Class describing a message action
|
||||||
class MessageAction {
|
class MessageAction {
|
||||||
|
/// returns a new instance of a [MessageAction]
|
||||||
|
MessageAction({
|
||||||
|
this.leading,
|
||||||
|
this.title,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
/// leading widget
|
/// leading widget
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
|
||||||
@@ -11,11 +18,4 @@ class MessageAction {
|
|||||||
|
|
||||||
/// callback called on tap
|
/// callback called on tap
|
||||||
final OnMessageTap? onTap;
|
final OnMessageTap? onTap;
|
||||||
|
|
||||||
/// returns a new instance of a [MessageAction]
|
|
||||||
MessageAction({
|
|
||||||
this.leading,
|
|
||||||
this.title,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,37 +8,11 @@ import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
|||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
|
||||||
import 'extension.dart';
|
/// Constructs a modal with actions for a message
|
||||||
import 'message_input.dart';
|
|
||||||
import 'message_widget.dart';
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
import 'stream_chat_theme.dart';
|
|
||||||
|
|
||||||
class MessageActionsModal extends StatefulWidget {
|
class MessageActionsModal extends StatefulWidget {
|
||||||
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
|
/// Constructor for creating a [MessageActionsModal] widget
|
||||||
final OnMessageTap? onThreadReplyTap;
|
|
||||||
final OnMessageTap? onReplyTap;
|
|
||||||
final Message message;
|
|
||||||
final MessageTheme messageTheme;
|
|
||||||
final bool showReactions;
|
|
||||||
final OnMessageTap? onCopyTap;
|
|
||||||
final bool showDeleteMessage;
|
|
||||||
final bool showCopyMessage;
|
|
||||||
final bool showEditMessage;
|
|
||||||
final bool showResendMessage;
|
|
||||||
final bool showReplyMessage;
|
|
||||||
final bool showThreadReplyMessage;
|
|
||||||
final bool showFlagButton;
|
|
||||||
final bool reverse;
|
|
||||||
final ShapeBorder? messageShape;
|
|
||||||
final ShapeBorder? attachmentShape;
|
|
||||||
final DisplayWidget showUserAvatar;
|
|
||||||
final BorderRadius? attachmentBorderRadiusGeometry;
|
|
||||||
|
|
||||||
/// List of custom actions
|
|
||||||
final List<MessageAction> customActions;
|
|
||||||
|
|
||||||
const MessageActionsModal({
|
const MessageActionsModal({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -63,6 +37,66 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.onCopyTap,
|
this.onCopyTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Builder for edit message
|
||||||
|
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
|
||||||
|
|
||||||
|
/// Callback for when thread reply is tapped
|
||||||
|
final OnMessageTap? onThreadReplyTap;
|
||||||
|
|
||||||
|
/// Callback for when reply is tapped
|
||||||
|
final OnMessageTap? onReplyTap;
|
||||||
|
|
||||||
|
/// Message in focus for actions
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// [MessageTheme] for message
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Flag for showing reactions
|
||||||
|
final bool showReactions;
|
||||||
|
|
||||||
|
/// Callback when copy is tapped
|
||||||
|
final OnMessageTap? onCopyTap;
|
||||||
|
|
||||||
|
/// Callback when delete is tapped
|
||||||
|
final bool showDeleteMessage;
|
||||||
|
|
||||||
|
/// Flag for showing copy action
|
||||||
|
final bool showCopyMessage;
|
||||||
|
|
||||||
|
/// Flag for showing edit action
|
||||||
|
final bool showEditMessage;
|
||||||
|
|
||||||
|
/// Flag for showing resend action
|
||||||
|
final bool showResendMessage;
|
||||||
|
|
||||||
|
/// Flag for showing reply action
|
||||||
|
final bool showReplyMessage;
|
||||||
|
|
||||||
|
/// Flag for showing thread reply action
|
||||||
|
final bool showThreadReplyMessage;
|
||||||
|
|
||||||
|
/// Flag for showing flag action
|
||||||
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Flag for reversing message
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// [ShapeBorder] to apply to the widget
|
||||||
|
final ShapeBorder? messageShape;
|
||||||
|
|
||||||
|
/// [ShapeBorder] to apply to attachment
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
|
/// Enum for displaying user avatar
|
||||||
|
final DisplayWidget showUserAvatar;
|
||||||
|
|
||||||
|
/// [BorderRadius] for attachment border
|
||||||
|
final BorderRadius? attachmentBorderRadiusGeometry;
|
||||||
|
|
||||||
|
/// List of custom actions
|
||||||
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageActionsModalState createState() => _MessageActionsModalState();
|
_MessageActionsModalState createState() => _MessageActionsModalState();
|
||||||
}
|
}
|
||||||
@@ -71,9 +105,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
bool _showActions = true;
|
bool _showActions = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => _showMessageOptionsModal();
|
||||||
return _showMessageOptionsModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _showMessageOptionsModal() {
|
Widget _showMessageOptionsModal() {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -119,16 +151,15 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
if (_showActions)
|
if (_showActions)
|
||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0.0, end: 1.0),
|
tween: Tween(begin: 0, end: 1),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOutBack,
|
curve: Curves.easeInOutBack,
|
||||||
builder: (context, val, snapshot) {
|
builder: (context, val, snapshot) => Transform.scale(
|
||||||
return Transform.scale(
|
|
||||||
scale: val,
|
scale: val,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: widget.reverse
|
crossAxisAlignment: widget.reverse
|
||||||
? CrossAxisAlignment.end
|
? CrossAxisAlignment.end
|
||||||
@@ -146,27 +177,27 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
: (divFactor > 1.0
|
: (divFactor > 1.0
|
||||||
? 0.0
|
? 0.0
|
||||||
: -(1.0 - divFactor)),
|
: -(1.0 - divFactor)),
|
||||||
0.0),
|
0),
|
||||||
child: ReactionPicker(
|
child: ReactionPicker(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: MessageWidget(
|
child: MessageWidget(
|
||||||
key: Key('MessageWidget'),
|
key: const Key('MessageWidget'),
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
widget.attachmentBorderRadiusGeometry,
|
widget.attachmentBorderRadiusGeometry,
|
||||||
message: widget.message.copyWith(
|
message: widget.message.copyWith(
|
||||||
text: widget.message.text!.length > 200
|
text: widget.message.text!.length > 200
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
? '${widget.message.text!.substring(0, 200)}...'
|
? '${widget.message.text!.substring(0, 200)}...'
|
||||||
: widget.message.text,
|
: widget.message.text,
|
||||||
),
|
),
|
||||||
messageTheme: widget.messageTheme,
|
messageTheme: widget.messageTheme,
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showThreadReplyIndicator: false,
|
|
||||||
showReplyMessage: false,
|
showReplyMessage: false,
|
||||||
showUserAvatar: widget.showUserAvatar,
|
showUserAvatar: widget.showUserAvatar,
|
||||||
attachmentPadding: EdgeInsets.all(
|
attachmentPadding: EdgeInsets.all(
|
||||||
@@ -176,22 +207,20 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
textPadding: EdgeInsets.symmetric(
|
textPadding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal: widget.message.text!.isOnlyEmoji
|
horizontal:
|
||||||
? 0
|
widget.message.text!.isOnlyEmoji ? 0 : 16.0,
|
||||||
: 16.0,
|
|
||||||
),
|
),
|
||||||
showReactionPickerIndicator:
|
showReactionPickerIndicator:
|
||||||
widget.showReactions &&
|
widget.showReactions &&
|
||||||
(widget.message.status ==
|
(widget.message.status ==
|
||||||
MessageSendingStatus.sent),
|
MessageSendingStatus.sent),
|
||||||
showInChannelIndicator: false,
|
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
shape: widget.messageShape,
|
shape: widget.messageShape,
|
||||||
attachmentShape: widget.attachmentShape,
|
attachmentShape: widget.attachmentShape,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: widget.reverse ? 0 : 40,
|
left: widget.reverse ? 0 : 40,
|
||||||
@@ -229,12 +258,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
_buildFlagButton(context),
|
_buildFlagButton(context),
|
||||||
if (widget.showDeleteMessage)
|
if (widget.showDeleteMessage)
|
||||||
_buildDeleteButton(context),
|
_buildDeleteButton(context),
|
||||||
...widget.customActions.map((action) {
|
...widget.customActions
|
||||||
return _buildCustomAction(
|
.map((action) => _buildCustomAction(
|
||||||
context,
|
context,
|
||||||
action,
|
action,
|
||||||
);
|
))
|
||||||
})
|
|
||||||
].insertBetween(
|
].insertBetween(
|
||||||
Container(
|
Container(
|
||||||
height: 1,
|
height: 1,
|
||||||
@@ -252,8 +280,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -263,23 +290,22 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
InkWell _buildCustomAction(
|
InkWell _buildCustomAction(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MessageAction messageAction,
|
MessageAction messageAction,
|
||||||
) {
|
) =>
|
||||||
return InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
messageAction.onTap?.call(widget.message);
|
messageAction.onTap?.call(widget.message);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
messageAction.leading ?? Offstage(),
|
messageAction.leading ?? const Offstage(),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
messageAction.title ?? Offstage(),
|
messageAction.title ?? const Offstage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void _showFlagDialog() async {
|
void _showFlagDialog() async {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
@@ -289,9 +315,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
title: 'Flag Message',
|
title: 'Flag Message',
|
||||||
icon: StreamSvgIcon.flag(
|
icon: StreamSvgIcon.flag(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
question:
|
question:
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
||||||
okText: 'FLAG',
|
okText: 'FLAG',
|
||||||
cancelText: 'CANCEL',
|
cancelText: 'CANCEL',
|
||||||
@@ -305,7 +332,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
context,
|
context,
|
||||||
icon: StreamSvgIcon.flag(
|
icon: StreamSvgIcon.flag(
|
||||||
color: theme.colorTheme.accentRed,
|
color: theme.colorTheme.accentRed,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
details: 'The message has been reported to a moderator.',
|
details: 'The message has been reported to a moderator.',
|
||||||
title: 'Message flagged',
|
title: 'Message flagged',
|
||||||
@@ -317,7 +344,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
context,
|
context,
|
||||||
icon: StreamSvgIcon.flag(
|
icon: StreamSvgIcon.flag(
|
||||||
color: theme.colorTheme.accentRed,
|
color: theme.colorTheme.accentRed,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
details: 'The message has been reported to a moderator.',
|
details: 'The message has been reported to a moderator.',
|
||||||
title: 'Message flagged',
|
title: 'Message flagged',
|
||||||
@@ -339,7 +366,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
title: 'Delete message',
|
title: 'Delete message',
|
||||||
icon: StreamSvgIcon.flag(
|
icon: StreamSvgIcon.flag(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
question: 'Are you sure you want to permanently delete this\nmessage?',
|
question: 'Are you sure you want to permanently delete this\nmessage?',
|
||||||
okText: 'DELETE',
|
okText: 'DELETE',
|
||||||
@@ -365,7 +392,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
context,
|
context,
|
||||||
icon: StreamSvgIcon.error(
|
icon: StreamSvgIcon.error(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
size: 24.0,
|
size: 24,
|
||||||
),
|
),
|
||||||
details: 'The operation couldn\'t be completed.',
|
details: 'The operation couldn\'t be completed.',
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
@@ -373,8 +400,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReplyButton(BuildContext context) {
|
Widget _buildReplyButton(BuildContext context) => InkWell(
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
if (widget.onReplyTap != null) {
|
if (widget.onReplyTap != null) {
|
||||||
@@ -382,7 +408,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.reply(
|
StreamSvgIcon.reply(
|
||||||
@@ -397,13 +423,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFlagButton(BuildContext context) {
|
Widget _buildFlagButton(BuildContext context) => InkWell(
|
||||||
return InkWell(
|
onTap: _showFlagDialog,
|
||||||
onTap: () => _showFlagDialog(),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.iconFlag(
|
StreamSvgIcon.iconFlag(
|
||||||
@@ -418,15 +442,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDeleteButton(BuildContext context) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
final isDeleteFailed =
|
final isDeleteFailed =
|
||||||
widget.message.status == MessageSendingStatus.failed_delete;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _showDeleteDialog(),
|
onTap: _showDeleteDialog,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.delete(
|
StreamSvgIcon.delete(
|
||||||
@@ -446,14 +469,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCopyButton(BuildContext context) {
|
Widget _buildCopyButton(BuildContext context) => InkWell(
|
||||||
return InkWell(
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
widget.onCopyTap?.call(widget.message);
|
widget.onCopyTap?.call(widget.message);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.copy(
|
StreamSvgIcon.copy(
|
||||||
@@ -469,16 +491,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEditMessage(BuildContext context) {
|
Widget _buildEditMessage(BuildContext context) => InkWell(
|
||||||
return InkWell(
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showEditBottomSheet(context);
|
_showEditBottomSheet(context);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.edit(
|
StreamSvgIcon.edit(
|
||||||
@@ -493,7 +513,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildResendMessage(BuildContext context) {
|
Widget _buildResendMessage(BuildContext context) {
|
||||||
final isUpdateFailed =
|
final isUpdateFailed =
|
||||||
@@ -509,7 +528,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.circleUp(
|
StreamSvgIcon.circleUp(
|
||||||
@@ -535,14 +554,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
StreamChatTheme.of(context).messageInputTheme.inputBackground,
|
StreamChatTheme.of(context).messageInputTheme.inputBackground,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.vertical,
|
direction: Axis.vertical,
|
||||||
@@ -555,14 +573,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: StreamSvgIcon.edit(
|
child: StreamSvgIcon.edit(
|
||||||
color: StreamChatTheme.of(context)
|
color:
|
||||||
.colorTheme
|
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||||
.greyGainsboro,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const Text(
|
||||||
'Edit Message',
|
'Edit Message',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
@@ -574,9 +591,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
widget.editMessageInputBuilder != null
|
if (widget.editMessageInputBuilder != null)
|
||||||
? widget.editMessageInputBuilder!(context, widget.message)
|
widget.editMessageInputBuilder!(context, widget.message)
|
||||||
: MessageInput(
|
else
|
||||||
|
MessageInput(
|
||||||
editMessage: widget.message,
|
editMessage: widget.message,
|
||||||
preMessageSending: (m) {
|
preMessageSending: (m) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
@@ -586,13 +604,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThreadReplyButton(BuildContext context) {
|
Widget _buildThreadReplyButton(BuildContext context) => InkWell(
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
if (widget.onThreadReplyTap != null) {
|
if (widget.onThreadReplyTap != null) {
|
||||||
@@ -600,7 +616,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.thread(
|
StreamSvgIcon.thread(
|
||||||
@@ -616,4 +632,3 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,34 +12,60 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/system_message.dart';
|
import 'package:stream_chat_flutter/src/system_message.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/swipeable.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Widget builder for message
|
||||||
import 'connection_status_builder.dart';
|
|
||||||
import 'date_divider.dart';
|
|
||||||
import 'extension.dart';
|
|
||||||
import 'swipeable.dart';
|
|
||||||
|
|
||||||
typedef MessageBuilder = Widget Function(
|
typedef MessageBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
MessageDetails,
|
MessageDetails,
|
||||||
List<Message>,
|
List<Message>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Widget builder for parent message
|
||||||
typedef ParentMessageBuilder = Widget Function(
|
typedef ParentMessageBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
Message?,
|
Message?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Widget builder for system message
|
||||||
typedef SystemMessageBuilder = Widget Function(
|
typedef SystemMessageBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
Message,
|
Message,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Widget builder for thread
|
||||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent);
|
typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent);
|
||||||
|
|
||||||
|
/// Callback for thread taps
|
||||||
typedef ThreadTapCallback = void Function(Message, Widget?);
|
typedef ThreadTapCallback = void Function(Message, Widget?);
|
||||||
|
|
||||||
|
/// Callback on message swiped
|
||||||
typedef OnMessageSwiped = void Function(Message);
|
typedef OnMessageSwiped = void Function(Message);
|
||||||
|
|
||||||
|
/// Callback on message tapped
|
||||||
typedef OnMessageTap = void Function(Message);
|
typedef OnMessageTap = void Function(Message);
|
||||||
|
|
||||||
|
/// Callback on reply tapped
|
||||||
typedef ReplyTapCallback = void Function(Message);
|
typedef ReplyTapCallback = void Function(Message);
|
||||||
|
|
||||||
|
/// Class for message details
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
|
/// Constructor for creating [MessageDetails]
|
||||||
|
MessageDetails(
|
||||||
|
BuildContext context,
|
||||||
|
this.message,
|
||||||
|
List<Message> messages,
|
||||||
|
this.index,
|
||||||
|
) {
|
||||||
|
isMyMessage = message.user?.id == StreamChat.of(context).user?.id;
|
||||||
|
isLastUser = index + 1 < messages.length &&
|
||||||
|
message.user?.id == messages[index + 1].user?.id;
|
||||||
|
isNextUser =
|
||||||
|
index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
/// True if the message belongs to the current user
|
/// True if the message belongs to the current user
|
||||||
bool? isMyMessage;
|
bool? isMyMessage;
|
||||||
|
|
||||||
@@ -54,19 +80,6 @@ class MessageDetails {
|
|||||||
|
|
||||||
/// The index of the message
|
/// The index of the message
|
||||||
int index;
|
int index;
|
||||||
|
|
||||||
MessageDetails(
|
|
||||||
BuildContext context,
|
|
||||||
this.message,
|
|
||||||
List<Message> messages,
|
|
||||||
this.index,
|
|
||||||
) {
|
|
||||||
isMyMessage = message.user?.id == StreamChat.of(context).user?.id;
|
|
||||||
isLastUser = index + 1 < messages.length &&
|
|
||||||
message.user?.id == messages[index + 1].user?.id;
|
|
||||||
isNextUser =
|
|
||||||
index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
@@ -104,14 +117,16 @@ class MessageDetails {
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
|
/// Make sure to have a [StreamChannel] ancestor in order to
|
||||||
|
/// provide the information about the channels.
|
||||||
/// The widget uses a [ListView.custom] to render the list of channels.
|
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget components render the ui based on the first
|
||||||
|
/// ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageListView extends StatefulWidget {
|
class MessageListView extends StatefulWidget {
|
||||||
/// Instantiate a new MessageListView
|
/// Instantiate a new MessageListView
|
||||||
MessageListView({
|
const MessageListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.showScrollToBottom = true,
|
this.showScrollToBottom = true,
|
||||||
this.messageBuilder,
|
this.messageBuilder,
|
||||||
@@ -158,10 +173,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
final ThreadBuilder? threadBuilder;
|
final ThreadBuilder? threadBuilder;
|
||||||
|
|
||||||
/// Function called when tapping on a thread
|
/// Function called when tapping on a thread
|
||||||
/// By default it calls [Navigator.push] using the widget built using [threadBuilder]
|
/// By default it calls [Navigator.push] using the widget
|
||||||
|
/// built using [threadBuilder]
|
||||||
final ThreadTapCallback? onThreadTap;
|
final ThreadTapCallback? onThreadTap;
|
||||||
|
|
||||||
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
|
/// If true will show a scroll to bottom message when there are new
|
||||||
|
/// messages and the scroll offset is not zero
|
||||||
final bool showScrollToBottom;
|
final bool showScrollToBottom;
|
||||||
|
|
||||||
/// Parent message in case of a thread
|
/// Parent message in case of a thread
|
||||||
@@ -201,8 +218,10 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Color used while highlighting initial message
|
/// Color used while highlighting initial message
|
||||||
final Color? messageHighlightColor;
|
final Color? messageHighlightColor;
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Flag for showing tile on header
|
||||||
final bool showConnectionStateTile;
|
final bool showConnectionStateTile;
|
||||||
|
|
||||||
/// Function called when messages are fetched
|
/// Function called when messages are fetched
|
||||||
@@ -214,8 +233,10 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Function used to build an empty widget
|
/// Function used to build an empty widget
|
||||||
final WidgetBuilder? emptyBuilder;
|
final WidgetBuilder? emptyBuilder;
|
||||||
|
|
||||||
/// Callback triggered when an error occurs while performing the given request.
|
/// Callback triggered when an error occurs while performing the
|
||||||
/// This parameter can be used to display an error message to users in the event
|
/// given request.
|
||||||
|
/// This parameter can be used to display an error message to
|
||||||
|
/// users in the event
|
||||||
/// of a connection failure.
|
/// of a connection failure.
|
||||||
final ErrorBuilder? errorWidgetBuilder;
|
final ErrorBuilder? errorWidgetBuilder;
|
||||||
|
|
||||||
@@ -223,10 +244,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
final bool Function(Message)? messageFilter;
|
final bool Function(Message)? messageFilter;
|
||||||
|
|
||||||
/// Attachment builders for the default message widget
|
/// Attachment builders for the default message widget
|
||||||
/// Please change this in the [MessageWidget] if you are using a custom implementation
|
/// Please change this in the [MessageWidget] if you are using a
|
||||||
|
/// custom implementation
|
||||||
final Map<String, AttachmentBuilder>? customAttachmentBuilders;
|
final Map<String, AttachmentBuilder>? customAttachmentBuilders;
|
||||||
|
|
||||||
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead)
|
/// Called when any message is tapped except a system message
|
||||||
|
/// (use [onSystemMessageTap] instead)
|
||||||
final OnMessageTap? onMessageTap;
|
final OnMessageTap? onMessageTap;
|
||||||
|
|
||||||
/// Called when system message is tapped
|
/// Called when system message is tapped
|
||||||
@@ -238,6 +261,7 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Customize the MessageWidget textBuilder
|
/// Customize the MessageWidget textBuilder
|
||||||
final void Function(BuildContext context, Message message)? textBuilder;
|
final void Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
|
/// Callback for when link is tapped
|
||||||
final void Function(String link)? onLinkTap;
|
final void Function(String link)? onLinkTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -257,9 +281,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
if (streamChannel!.initialMessageId != null) {
|
if (streamChannel!.initialMessageId != null) {
|
||||||
final messages = streamChannel!.channel.state!.messages;
|
final messages = streamChannel!.channel.state!.messages;
|
||||||
final totalMessages = messages.length;
|
final totalMessages = messages.length;
|
||||||
final messageIndex = messages.indexWhere((e) {
|
final messageIndex =
|
||||||
return e.id == streamChannel!.initialMessageId;
|
messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
|
||||||
});
|
|
||||||
final index = totalMessages - messageIndex;
|
final index = totalMessages - messageIndex;
|
||||||
if (index != 0) return index - 1;
|
if (index != 0) return index - 1;
|
||||||
return index;
|
return index;
|
||||||
@@ -272,9 +295,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isInitialMessage(String id) {
|
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
||||||
return streamChannel!.initialMessageId == id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _upToDate => streamChannel!.channel.state!.isUpToDate;
|
bool get _upToDate => streamChannel!.channel.state!.isUpToDate;
|
||||||
|
|
||||||
@@ -295,50 +316,46 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final MessageListController _messageListController = MessageListController();
|
final MessageListController _messageListController = MessageListController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => MessageListCore(
|
||||||
return MessageListCore(
|
|
||||||
messageFilter: widget.messageFilter,
|
messageFilter: widget.messageFilter,
|
||||||
loadingBuilder: widget.loadingBuilder ??
|
loadingBuilder: widget.loadingBuilder ??
|
||||||
(context) {
|
(context) => const Center(
|
||||||
return Center(
|
child: CircularProgressIndicator(),
|
||||||
child: const CircularProgressIndicator(),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
emptyBuilder: widget.emptyBuilder ??
|
emptyBuilder: widget.emptyBuilder ??
|
||||||
(context) {
|
(context) => Center(
|
||||||
return Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'No chats here yet...',
|
'No chats here yet...',
|
||||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
style: StreamChatTheme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.footnote
|
||||||
|
.copyWith(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
.withOpacity(.5)),
|
.withOpacity(.5)),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
messageListBuilder: widget.messageListBuilder ??
|
messageListBuilder: widget.messageListBuilder ??
|
||||||
(context, list) {
|
(context, list) => _buildListView(list),
|
||||||
return _buildListView(list);
|
|
||||||
},
|
|
||||||
messageListController: _messageListController,
|
messageListController: _messageListController,
|
||||||
parentMessage: widget.parentMessage,
|
parentMessage: widget.parentMessage,
|
||||||
showScrollToBottom: widget.showScrollToBottom,
|
showScrollToBottom: widget.showScrollToBottom,
|
||||||
errorWidgetBuilder: widget.errorWidgetBuilder ??
|
errorWidgetBuilder: widget.errorWidgetBuilder ??
|
||||||
(BuildContext context, Object error) {
|
(BuildContext context, Object error) => Center(
|
||||||
return Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'Something went wrong',
|
'Something went wrong',
|
||||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
style: StreamChatTheme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.footnote
|
||||||
|
.copyWith(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.black
|
.black
|
||||||
.withOpacity(.5)),
|
.withOpacity(.5)),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListView(List<Message> data) {
|
Widget _buildListView(List<Message> data) {
|
||||||
messages = data;
|
messages = data;
|
||||||
@@ -384,6 +401,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
showMessage: widget.showConnectionStateTile ? showStatus : false,
|
showMessage: widget.showConnectionStateTile ? showStatus : false,
|
||||||
tileAnchor: Alignment.topCenter,
|
tileAnchor: Alignment.topCenter,
|
||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
@@ -418,7 +436,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
child: ScrollablePositionedList.separated(
|
child: ScrollablePositionedList.separated(
|
||||||
key: ValueKey(initialIndex! + initialAlignment!),
|
key: ValueKey(initialIndex! + initialAlignment!),
|
||||||
itemPositionsListener: _itemPositionListener,
|
itemPositionsListener: _itemPositionListener,
|
||||||
addAutomaticKeepAlives: true,
|
|
||||||
initialScrollIndex: initialIndex ?? 0,
|
initialScrollIndex: initialIndex ?? 0,
|
||||||
initialAlignment: initialAlignment ?? 0,
|
initialAlignment: initialAlignment ?? 0,
|
||||||
physics: widget.scrollPhysics,
|
physics: widget.scrollPhysics,
|
||||||
@@ -427,8 +444,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
itemCount:
|
itemCount:
|
||||||
messages.length + 2 + (_isThreadConversation ? 1 : 0),
|
messages.length + 2 + (_isThreadConversation ? 1 : 0),
|
||||||
separatorBuilder: (context, i) {
|
separatorBuilder: (context, i) {
|
||||||
if (i == messages.length) return Offstage();
|
if (i == messages.length) return const Offstage();
|
||||||
if (i == 0) return SizedBox(height: 30);
|
if (i == 0) return const SizedBox(height: 30);
|
||||||
if (i == messages.length + 1) {
|
if (i == messages.length + 1) {
|
||||||
final replyCount = widget.parentMessage!.replyCount;
|
final replyCount = widget.parentMessage!.replyCount;
|
||||||
return Container(
|
return Container(
|
||||||
@@ -437,8 +454,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
StreamChatTheme.of(context).colorTheme.bgGradient,
|
StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
|
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
@@ -464,7 +482,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
dateTime: nextMessage.createdAt.toLocal(),
|
dateTime: nextMessage.createdAt.toLocal(),
|
||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: divider,
|
child: divider,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -482,9 +500,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
!isNextUserSame ||
|
!isNextUserSame ||
|
||||||
isThread ||
|
isThread ||
|
||||||
isDeleted) {
|
isDeleted) {
|
||||||
return SizedBox(height: 8);
|
return const SizedBox(height: 8);
|
||||||
}
|
}
|
||||||
return SizedBox(height: 2);
|
return const SizedBox(height: 2);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == messages.length + 2) {
|
if (i == messages.length + 2) {
|
||||||
@@ -554,19 +572,19 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
),
|
),
|
||||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 20.0,
|
top: 20,
|
||||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||||
valueListenable: _itemPositionListener.itemPositions,
|
valueListenable: _itemPositionListener.itemPositions,
|
||||||
builder: (context, values, _) {
|
builder: (context, values, _) {
|
||||||
final items = _itemPositionListener.itemPositions.value;
|
final items = _itemPositionListener.itemPositions.value;
|
||||||
if (items.isEmpty || messages.isEmpty) {
|
if (items.isEmpty || messages.isEmpty) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = _getTopElement(values).index;
|
var index = _getTopElement(values).index;
|
||||||
|
|
||||||
if (index > messages.length) {
|
if (index > messages.length) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == messages.length) {
|
if (index == messages.length) {
|
||||||
@@ -588,19 +606,15 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _paginateData(
|
Future<void> _paginateData(
|
||||||
StreamChannelState? channel, QueryDirection direction) {
|
StreamChannelState? channel, QueryDirection direction) =>
|
||||||
return _messageListController.paginateData!(direction: direction);
|
_messageListController.paginateData!(direction: direction);
|
||||||
}
|
|
||||||
|
|
||||||
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
|
ItemPosition _getTopElement(Iterable<ItemPosition> values) => values
|
||||||
return values
|
|
||||||
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
|
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
|
||||||
.reduce((ItemPosition max, ItemPosition position) =>
|
.reduce((ItemPosition max, ItemPosition position) =>
|
||||||
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScrollToBottom() {
|
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||||
return StreamBuilder<Tuple2<bool, int>>(
|
|
||||||
stream: Rx.combineLatest2(
|
stream: Rx.combineLatest2(
|
||||||
streamChannel!.channel.state!.isUpToDateStream,
|
streamChannel!.channel.state!.isUpToDateStream,
|
||||||
streamChannel!.channel.state!.unreadCountStream,
|
streamChannel!.channel.state!.unreadCountStream,
|
||||||
@@ -608,14 +622,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
),
|
),
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
} else if (!snapshot.hasData) {
|
} else if (!snapshot.hasData) {
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
final isUpToDate = snapshot.data!.item1;
|
final isUpToDate = snapshot.data!.item1;
|
||||||
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
||||||
if (!showScrollToBottom) {
|
if (!showScrollToBottom) {
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
final unreadCount = snapshot.data!.item2;
|
final unreadCount = snapshot.data!.item2;
|
||||||
final showUnreadCount = unreadCount > 0 &&
|
final showUnreadCount = unreadCount > 0 &&
|
||||||
@@ -643,7 +657,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
setState(() => _showScrollToBottom = false);
|
setState(() => _showScrollToBottom = false);
|
||||||
_scrollController!.scrollTo(
|
_scrollController!.scrollTo(
|
||||||
index: 0,
|
index: 0,
|
||||||
duration: Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -660,10 +674,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
top: -10,
|
top: -10,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(3.0),
|
padding: const EdgeInsets.all(3),
|
||||||
child: Text(
|
child: Text(
|
||||||
'$unreadCount',
|
'$unreadCount',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -676,7 +690,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLoadingIndicator(
|
Widget _buildLoadingIndicator(
|
||||||
StreamChannelState? streamChannel,
|
StreamChannelState? streamChannel,
|
||||||
@@ -686,7 +699,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
? streamChannel!.queryTopMessages
|
? streamChannel!.queryTopMessages
|
||||||
: streamChannel!.queryBottomMessages;
|
: streamChannel!.queryBottomMessages;
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<bool>(
|
||||||
key: Key('LOADING-INDICATOR'),
|
key: const Key('LOADING-INDICATOR'),
|
||||||
stream: stream,
|
stream: stream,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@@ -696,24 +709,24 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.accentRed
|
.accentRed
|
||||||
.withOpacity(.2),
|
.withOpacity(.2),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Text('Error loading messages'),
|
child: Text('Error loading messages'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.data!) {
|
if (!snapshot.data!) {
|
||||||
if (!_isThreadConversation && direction == QueryDirection.top) {
|
if (!_isThreadConversation && direction == QueryDirection.top) {
|
||||||
return Container(
|
return const SizedBox(
|
||||||
height: 52,
|
height: 52,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return Center(
|
return const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8),
|
||||||
child: const CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -729,7 +742,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Widget messageWidget;
|
Widget messageWidget;
|
||||||
if (widget.messageBuilder != null) {
|
if (widget.messageBuilder != null) {
|
||||||
messageWidget = Builder(
|
messageWidget = Builder(
|
||||||
key: ValueKey<String>('TOP-MESSAGE'),
|
key: const ValueKey<String>('TOP-MESSAGE'),
|
||||||
builder: (_) => widget.messageBuilder!(
|
builder: (_) => widget.messageBuilder!(
|
||||||
context,
|
context,
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
@@ -799,8 +812,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||||
|
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
showThreadReplyIndicator: false,
|
|
||||||
showInChannelIndicator: false,
|
|
||||||
showReplyMessage: false,
|
showReplyMessage: false,
|
||||||
showResendMessage: false,
|
showResendMessage: false,
|
||||||
showThreadReplyMessage: false,
|
showThreadReplyMessage: false,
|
||||||
@@ -810,17 +821,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showUsername: !isMyMessage,
|
showUsername: !isMyMessage,
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
onThreadTap: _onThreadTap as void Function(Message)?,
|
onThreadTap: _onThreadTap as void Function(Message)?,
|
||||||
borderRadiusGeometry: BorderRadius.only(
|
borderRadiusGeometry: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
bottomLeft: Radius.circular(2),
|
bottomLeft: Radius.circular(2),
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
bottomRight: Radius.circular(16),
|
bottomRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
textPadding: EdgeInsets.symmetric(
|
textPadding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal: isOnlyEmoji ? 0 : 16.0,
|
horizontal: isOnlyEmoji ? 0 : 16.0,
|
||||||
),
|
),
|
||||||
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
|
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
|
||||||
@@ -889,8 +900,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final channel = streamChannel!.channel;
|
final channel = streamChannel!.channel;
|
||||||
final readList = channel.state?.read?.where((read) {
|
final readList = channel.state?.read?.where((read) {
|
||||||
if (read.user.id == userId) return false;
|
if (read.user.id == userId) return false;
|
||||||
return (read.lastRead.isAfter(message.createdAt) ||
|
return read.lastRead.isAfter(message.createdAt) ||
|
||||||
read.lastRead.isAtSameMomentAs(message.createdAt));
|
read.lastRead.isAtSameMomentAs(message.createdAt);
|
||||||
}).toList() ??
|
}).toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
@@ -940,7 +951,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showReactions: !message.isDeleted,
|
showReactions: !message.isDeleted,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
showInChannelIndicator: showInChannelIndicator,
|
showInChannelIndicator: showInChannelIndicator,
|
||||||
showThreadReplyIndicator: showThreadReplyIndicator,
|
showThreadReplyIndicator: showThreadReplyIndicator,
|
||||||
showUsername: showUsername,
|
showUsername: showUsername,
|
||||||
@@ -948,6 +959,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
showSendingIndicator: showSendingIndicator,
|
showSendingIndicator: showSendingIndicator,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
onQuotedMessageTap: (quotedMessageId) async {
|
onQuotedMessageTap: (quotedMessageId) async {
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
final scrollToIndex = () {
|
final scrollToIndex = () {
|
||||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||||
_scrollController?.scrollTo(
|
_scrollController?.scrollTo(
|
||||||
@@ -987,17 +999,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
),
|
),
|
||||||
attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
|
attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
|
||||||
borderRadiusGeometry: BorderRadius.only(
|
borderRadiusGeometry: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: const Radius.circular(16),
|
||||||
bottomLeft: Radius.circular(
|
bottomLeft: Radius.circular(
|
||||||
(timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage)
|
(timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage)
|
||||||
? 0
|
? 0
|
||||||
: 16,
|
: 16,
|
||||||
),
|
),
|
||||||
topRight: Radius.circular(16),
|
topRight: const Radius.circular(16),
|
||||||
bottomRight: Radius.circular(16),
|
bottomRight: const Radius.circular(16),
|
||||||
),
|
),
|
||||||
textPadding: EdgeInsets.symmetric(
|
textPadding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal: isOnlyEmoji ? 0 : 16.0,
|
horizontal: isOnlyEmoji ? 0 : 16.0,
|
||||||
),
|
),
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
@@ -1034,7 +1046,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
!message.isEphemeral &&
|
!message.isEphemeral &&
|
||||||
widget.onMessageSwiped != null) {
|
widget.onMessageSwiped != null) {
|
||||||
child = Container(
|
child = Container(
|
||||||
decoration: BoxDecoration(),
|
decoration: const BoxDecoration(),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Swipeable(
|
child: Swipeable(
|
||||||
onSwipeEnd: () {
|
onSwipeEnd: () {
|
||||||
@@ -1062,14 +1074,12 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
),
|
),
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
onEnd: () => initialMessageHighlightComplete = true,
|
onEnd: () => initialMessageHighlightComplete = true,
|
||||||
builder: (_, color, child) {
|
builder: (_, color, child) => Container(
|
||||||
return Container(
|
|
||||||
color: color,
|
color: color,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1127,19 +1137,18 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
_onThreadTap = (Message message) {
|
_onThreadTap = (Message message) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) {
|
MaterialPageRoute(
|
||||||
return StreamBuilder<Message>(
|
builder: (_) => StreamBuilder<Message>(
|
||||||
stream: streamChannel!.channel.state!.messagesStream.map(
|
stream: streamChannel!.channel.state!.messagesStream.map(
|
||||||
(messages) =>
|
(messages) =>
|
||||||
messages!.firstWhere((m) => m.id == message.id)),
|
messages!.firstWhere((m) => m.id == message.id)),
|
||||||
initialData: message,
|
initialData: message,
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: streamChannel!.channel,
|
channel: streamChannel!.channel,
|
||||||
child: widget.threadBuilder!(context, snapshot.data),
|
child: widget.threadBuilder!(context, snapshot.data),
|
||||||
);
|
),
|
||||||
});
|
),
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,11 @@ import 'package:stream_chat_flutter/src/stream_chat.dart';
|
|||||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
|
||||||
import 'extension.dart';
|
/// Modal widget for displaying message reactions
|
||||||
import 'message_widget.dart';
|
|
||||||
import 'stream_chat_theme.dart';
|
|
||||||
|
|
||||||
class MessageReactionsModal extends StatelessWidget {
|
class MessageReactionsModal extends StatelessWidget {
|
||||||
final Message message;
|
/// Constructor for creating a [MessageReactionsModal] reactions
|
||||||
final MessageTheme messageTheme;
|
|
||||||
final bool reverse;
|
|
||||||
final bool showReactions;
|
|
||||||
final DisplayWidget showUserAvatar;
|
|
||||||
final ShapeBorder? messageShape;
|
|
||||||
final ShapeBorder? attachmentShape;
|
|
||||||
final void Function(User)? onUserAvatarTap;
|
|
||||||
final BorderRadius? attachmentBorderRadiusGeometry;
|
|
||||||
|
|
||||||
const MessageReactionsModal({
|
const MessageReactionsModal({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -36,6 +25,33 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentBorderRadiusGeometry,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Message to display reactions of
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// [MessageTheme] to apply to [message]
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// Flag to reverse message
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// Flag to show reactions on message
|
||||||
|
final bool showReactions;
|
||||||
|
|
||||||
|
/// Enum to change user avatar config
|
||||||
|
final DisplayWidget showUserAvatar;
|
||||||
|
|
||||||
|
/// [ShapeBorder] to apply to message
|
||||||
|
final ShapeBorder? messageShape;
|
||||||
|
|
||||||
|
/// [ShapeBorder] to apply to attachment
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
|
/// Callback when user avatar is tapped
|
||||||
|
final void Function(User)? onUserAvatarTap;
|
||||||
|
|
||||||
|
/// [BorderRadius] to apply to attachments
|
||||||
|
final BorderRadius? attachmentBorderRadiusGeometry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -59,8 +75,8 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||||
|
|
||||||
return TweenAnimationBuilder<double>(
|
return TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0.0, end: 1.0),
|
tween: Tween(begin: 0, end: 1),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOutBack,
|
curve: Curves.easeInOutBack,
|
||||||
builder: (context, val, snapshot) {
|
builder: (context, val, snapshot) {
|
||||||
final hasFileAttachment =
|
final hasFileAttachment =
|
||||||
@@ -86,7 +102,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -102,7 +118,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
: (divFactor > 1.0
|
: (divFactor > 1.0
|
||||||
? 0.0
|
? 0.0
|
||||||
: -(1.0 - divFactor)),
|
: -(1.0 - divFactor)),
|
||||||
0.0),
|
0),
|
||||||
child: ReactionPicker(
|
child: ReactionPicker(
|
||||||
message: message,
|
message: message,
|
||||||
),
|
),
|
||||||
@@ -110,7 +126,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: MessageWidget(
|
child: MessageWidget(
|
||||||
key: Key('MessageWidget'),
|
key: const Key('MessageWidget'),
|
||||||
reverse: reverse,
|
reverse: reverse,
|
||||||
message: message.copyWith(
|
message: message.copyWith(
|
||||||
text: message.text!.length > 200
|
text: message.text!.length > 200
|
||||||
@@ -121,7 +137,6 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showThreadReplyIndicator: false,
|
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
@@ -133,9 +148,8 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
attachmentPadding: EdgeInsets.all(
|
attachmentPadding: EdgeInsets.all(
|
||||||
hasFileAttachment ? 4 : 2,
|
hasFileAttachment ? 4 : 2,
|
||||||
),
|
),
|
||||||
showInChannelIndicator: false,
|
|
||||||
textPadding: EdgeInsets.symmetric(
|
textPadding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
horizontal:
|
horizontal:
|
||||||
message.text!.isOnlyEmoji ? 0 : 16.0,
|
message.text!.isOnlyEmoji ? 0 : 16.0,
|
||||||
),
|
),
|
||||||
@@ -184,7 +198,6 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
runSpacing: 16,
|
runSpacing: 16,
|
||||||
alignment: WrapAlignment.start,
|
|
||||||
children: message.latestReactions!
|
children: message.latestReactions!
|
||||||
.map((e) => _buildReaction(
|
.map((e) => _buildReaction(
|
||||||
e,
|
e,
|
||||||
@@ -208,14 +221,12 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
) {
|
) {
|
||||||
final isCurrentUser = reaction.user?.id == currentUser.id;
|
final isCurrentUser = reaction.user?.id == currentUser.id;
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(Size(
|
constraints: BoxConstraints.loose(const Size(
|
||||||
64,
|
64,
|
||||||
98,
|
98,
|
||||||
)),
|
)),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
@@ -223,11 +234,11 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
UserAvatar(
|
UserAvatar(
|
||||||
onTap: onUserAvatarTap,
|
onTap: onUserAvatarTap,
|
||||||
user: reaction.user!,
|
user: reaction.user!,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 64,
|
height: 64,
|
||||||
width: 64,
|
width: 64,
|
||||||
),
|
),
|
||||||
onlineIndicatorConstraints: BoxConstraints.tightFor(
|
onlineIndicatorConstraints: const BoxConstraints.tightFor(
|
||||||
height: 12,
|
height: 12,
|
||||||
width: 12,
|
width: 12,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
|
|
||||||
/// It shows the current [Message] preview.
|
/// It shows the current [Message] preview.
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's the default item used by [MessageSearchListView].
|
/// Usually you don't use this widget as it's the default item used by
|
||||||
|
/// [MessageSearchListView].
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget renders the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageSearchItem extends StatelessWidget {
|
class MessageSearchItem extends StatelessWidget {
|
||||||
/// Instantiate a new MessageSearchItem
|
/// Instantiate a new MessageSearchItem
|
||||||
@@ -38,7 +40,7 @@ class MessageSearchItem extends StatelessWidget {
|
|||||||
leading: UserAvatar(
|
leading: UserAvatar(
|
||||||
user: user,
|
user: user,
|
||||||
showOnlineStatus: showOnlineStatus,
|
showOnlineStatus: showOnlineStatus,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
@@ -69,7 +71,7 @@ class MessageSearchItem extends StatelessWidget {
|
|||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildSubtitle(context, message)),
|
Expanded(child: _buildSubtitle(context, message)),
|
||||||
SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_buildDate(context, message),
|
_buildDate(context, message),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -147,9 +149,9 @@ class MessageSearchItem extends StatelessWidget {
|
|||||||
List<Attachment> attachments,
|
List<Attachment> attachments,
|
||||||
TextStyle? normalTextStyle,
|
TextStyle? normalTextStyle,
|
||||||
TextStyle? mentionsTextStyle) {
|
TextStyle? mentionsTextStyle) {
|
||||||
var textList = text.split(' ');
|
final textList = text.split(' ');
|
||||||
var resList = <TextSpan>[];
|
final resList = <TextSpan>[];
|
||||||
for (var e in textList) {
|
for (final e in textList) {
|
||||||
if (mentions.isNotEmpty &&
|
if (mentions.isNotEmpty &&
|
||||||
mentions.any((element) => '@${element.name}' == e)) {
|
mentions.any((element) => '@${element.name}' == e)) {
|
||||||
resList.add(TextSpan(
|
resList.add(TextSpan(
|
||||||
@@ -166,7 +168,7 @@ class MessageSearchItem extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
resList.add(TextSpan(
|
resList.add(TextSpan(
|
||||||
text: e == textList.last ? '$e' : '$e ',
|
text: e == textList.last ? e : '$e ',
|
||||||
style: normalTextStyle,
|
style: normalTextStyle,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_search_item.dart';
|
import 'package:stream_chat_flutter/src/message_search_item.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import '../stream_chat_flutter.dart';
|
|
||||||
|
|
||||||
/// Callback called when tapping on a user
|
/// Callback called when tapping on a user
|
||||||
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
||||||
@@ -43,10 +42,12 @@ typedef EmptyMessageSearchBuilder = Widget Function(
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
|
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the
|
||||||
|
/// information about the messages.
|
||||||
/// The widget uses a [ListView.separated] to render the list of messages.
|
/// The widget uses a [ListView.separated] to render the list of messages.
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget components render the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageSearchListView extends StatefulWidget {
|
class MessageSearchListView extends StatefulWidget {
|
||||||
/// Instantiate a new MessageSearchListView
|
/// Instantiate a new MessageSearchListView
|
||||||
@@ -78,8 +79,10 @@ class MessageSearchListView extends StatefulWidget {
|
|||||||
final Filter filters;
|
final Filter filters;
|
||||||
|
|
||||||
/// The sorting used for the channels matching the filters.
|
/// The sorting used for the channels matching the filters.
|
||||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
/// Sorting is based on field and direction, multiple sorting options
|
||||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
/// can be provided.
|
||||||
|
/// You can sort based on last_updated, last_message_at, updated_at,
|
||||||
|
/// created_at or member_count.
|
||||||
/// Direction can be ascending or descending.
|
/// Direction can be ascending or descending.
|
||||||
final List<SortOption>? sortOptions;
|
final List<SortOption>? sortOptions;
|
||||||
|
|
||||||
@@ -109,6 +112,7 @@ class MessageSearchListView extends StatefulWidget {
|
|||||||
/// Set it to false to disable the pull-to-refresh widget
|
/// Set it to false to disable the pull-to-refresh widget
|
||||||
final bool pullToRefresh;
|
final bool pullToRefresh;
|
||||||
|
|
||||||
|
/// Show error tile on top
|
||||||
final bool showErrorTile;
|
final bool showErrorTile;
|
||||||
|
|
||||||
/// The builder that is used when the search messages are fetched
|
/// The builder that is used when the search messages are fetched
|
||||||
@@ -132,8 +136,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
MessageSearchListController();
|
MessageSearchListController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => MessageSearchListCore(
|
||||||
return MessageSearchListCore(
|
|
||||||
filters: widget.filters,
|
filters: widget.filters,
|
||||||
sortOptions: widget.sortOptions,
|
sortOptions: widget.sortOptions,
|
||||||
messageQuery: widget.messageQuery,
|
messageQuery: widget.messageQuery,
|
||||||
@@ -141,23 +144,20 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
messageFilters: widget.messageFilters,
|
messageFilters: widget.messageFilters,
|
||||||
messageSearchListController: _messageSearchListController,
|
messageSearchListController: _messageSearchListController,
|
||||||
emptyBuilder: widget.emptyBuilder ??
|
emptyBuilder: widget.emptyBuilder ??
|
||||||
(context) {
|
(context) => LayoutBuilder(
|
||||||
return LayoutBuilder(
|
builder: (context, viewportConstraints) =>
|
||||||
builder: (context, viewportConstraints) {
|
SingleChildScrollView(
|
||||||
return SingleChildScrollView(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: viewportConstraints.maxHeight,
|
minHeight: viewportConstraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Text('There are no messages currently'),
|
child: Text('There are no messages currently'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
errorBuilder: widget.errorBuilder ??
|
errorBuilder: widget.errorBuilder ??
|
||||||
(BuildContext context, dynamic error) {
|
(BuildContext context, dynamic error) {
|
||||||
if (error is Error) {
|
if (error is Error) {
|
||||||
@@ -172,36 +172,27 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loadingBuilder: widget.loadingBuilder ??
|
loadingBuilder: widget.loadingBuilder ??
|
||||||
(context) {
|
(context) => LayoutBuilder(
|
||||||
return LayoutBuilder(
|
builder: (context, viewportConstraints) =>
|
||||||
builder: (context, viewportConstraints) {
|
SingleChildScrollView(
|
||||||
return SingleChildScrollView(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: viewportConstraints.maxHeight,
|
minHeight: viewportConstraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
childBuilder: widget.childBuilder ?? _buildListView,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childBuilder: widget.childBuilder ??
|
|
||||||
(list) {
|
|
||||||
return _buildListView(list);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _separatorBuilder(BuildContext context, int index) {
|
Widget _separatorBuilder(BuildContext context, int index) => Container(
|
||||||
return Container(
|
|
||||||
height: 1,
|
height: 1,
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _listItemBuilder(
|
Widget _listItemBuilder(
|
||||||
BuildContext context, GetMessageResponse getMessageResponse) {
|
BuildContext context, GetMessageResponse getMessageResponse) {
|
||||||
@@ -227,8 +218,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.accentRed
|
.accentRed
|
||||||
.withOpacity(.2),
|
.withOpacity(.2),
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('Error loading messages'),
|
child: Text('Error loading messages'),
|
||||||
),
|
),
|
||||||
@@ -237,9 +228,11 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
height: 100,
|
height: 100,
|
||||||
padding: EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: snapshot.data! ? CircularProgressIndicator() : Container(),
|
child: snapshot.data!
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Container(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -249,7 +242,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
final items = data;
|
final items = data;
|
||||||
|
|
||||||
Widget child = ListView.separated(
|
Widget child = ListView.separated(
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||||
separatorBuilder: (_, index) {
|
separatorBuilder: (_, index) {
|
||||||
if (widget.separatorBuilder != null) {
|
if (widget.separatorBuilder != null) {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:collection/collection.dart' show IterableExtension;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'stream_chat_theme.dart';
|
/// Text widget to display in message
|
||||||
import 'utils.dart';
|
|
||||||
|
|
||||||
class MessageText extends StatelessWidget {
|
class MessageText extends StatelessWidget {
|
||||||
|
/// Constructor for creating a [MessageText] widget
|
||||||
const MessageText({
|
const MessageText({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -15,9 +15,16 @@ class MessageText extends StatelessWidget {
|
|||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Message whose text is to be displayed
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// Callback for when mention is tapped
|
||||||
final void Function(User)? onMentionTap;
|
final void Function(User)? onMentionTap;
|
||||||
|
|
||||||
|
/// Callback for when link is tapped
|
||||||
final void Function(String)? onLinkTap;
|
final void Function(String)? onLinkTap;
|
||||||
|
|
||||||
|
/// [MessageTheme] whose text theme is to be applied
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -71,6 +78,7 @@ class MessageText extends StatelessWidget {
|
|||||||
|
|
||||||
String _replaceMentions(String text) {
|
String _replaceMentions(String text) {
|
||||||
message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) {
|
message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
text = text.replaceAll(
|
text = text.replaceAll(
|
||||||
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
|
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
|||||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/image_group.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
|
||||||
import 'attachment/attachment.dart';
|
/// Widget builder for building attachments
|
||||||
import 'extension.dart';
|
|
||||||
import 'image_group.dart';
|
|
||||||
import 'message_text.dart';
|
|
||||||
|
|
||||||
typedef AttachmentBuilder = Widget Function(
|
typedef AttachmentBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
Message,
|
Message,
|
||||||
List<Attachment>,
|
List<Attachment>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Callback for when quoted message is tapped
|
||||||
typedef OnQuotedMessageTap = void Function(String?);
|
typedef OnQuotedMessageTap = void Function(String?);
|
||||||
|
|
||||||
/// The display behaviour of a widget
|
/// The display behaviour of a widget
|
||||||
@@ -45,119 +45,13 @@ enum DisplayWidget {
|
|||||||
///
|
///
|
||||||
/// It shows a message with reactions, replies and user avatar.
|
/// It shows a message with reactions, replies and user avatar.
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's the default message widget used by [MessageListView].
|
/// Usually you don't use this widget as it's the default message widget used by
|
||||||
|
/// [MessageListView].
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget components render the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageWidget extends StatefulWidget {
|
class MessageWidget extends StatefulWidget {
|
||||||
/// Function called on mention tap
|
|
||||||
final void Function(User)? onMentionTap;
|
|
||||||
|
|
||||||
/// The function called when tapping on replies
|
|
||||||
final void Function(Message)? onThreadTap;
|
|
||||||
final void Function(Message)? onReplyTap;
|
|
||||||
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
|
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
|
||||||
|
|
||||||
/// Function called on long press
|
|
||||||
final void Function(BuildContext, Message)? onMessageActions;
|
|
||||||
|
|
||||||
/// The message
|
|
||||||
final Message message;
|
|
||||||
|
|
||||||
/// The message theme
|
|
||||||
final MessageTheme messageTheme;
|
|
||||||
|
|
||||||
/// If true the widget will be mirrored
|
|
||||||
final bool reverse;
|
|
||||||
|
|
||||||
/// The shape of the message text
|
|
||||||
final ShapeBorder? shape;
|
|
||||||
|
|
||||||
/// The shape of an attachment
|
|
||||||
final ShapeBorder? attachmentShape;
|
|
||||||
|
|
||||||
/// The borderside of the message text
|
|
||||||
final BorderSide? borderSide;
|
|
||||||
|
|
||||||
/// The borderside of an attachment
|
|
||||||
final BorderSide? attachmentBorderSide;
|
|
||||||
|
|
||||||
/// The border radius of the message text
|
|
||||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
|
||||||
|
|
||||||
/// The border radius of an attachment
|
|
||||||
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
|
|
||||||
|
|
||||||
/// The padding of the widget
|
|
||||||
final EdgeInsetsGeometry? padding;
|
|
||||||
|
|
||||||
/// The internal padding of the message text
|
|
||||||
final EdgeInsets textPadding;
|
|
||||||
|
|
||||||
/// The internal padding of an attachment
|
|
||||||
final EdgeInsetsGeometry attachmentPadding;
|
|
||||||
|
|
||||||
/// It controls the display behaviour of the user avatar
|
|
||||||
final DisplayWidget showUserAvatar;
|
|
||||||
|
|
||||||
/// It controls the display behaviour of the sending indicator
|
|
||||||
final bool showSendingIndicator;
|
|
||||||
|
|
||||||
/// If true the widget will show the reactions
|
|
||||||
final bool showReactions;
|
|
||||||
|
|
||||||
final bool allRead;
|
|
||||||
|
|
||||||
/// If true the widget will show the thread reply indicator
|
|
||||||
final bool showThreadReplyIndicator;
|
|
||||||
|
|
||||||
/// If true the widget will show the show in channel indicator
|
|
||||||
final bool showInChannelIndicator;
|
|
||||||
|
|
||||||
/// The function called when tapping on UserAvatar
|
|
||||||
final void Function(User)? onUserAvatarTap;
|
|
||||||
|
|
||||||
/// The function called when tapping on a link
|
|
||||||
final void Function(String)? onLinkTap;
|
|
||||||
|
|
||||||
/// Used in [MessageReactionsModal] and [MessageActionsModal]
|
|
||||||
final bool showReactionPickerIndicator;
|
|
||||||
|
|
||||||
final List<Read>? readList;
|
|
||||||
|
|
||||||
final ShowMessageCallback? onShowMessage;
|
|
||||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
|
||||||
|
|
||||||
/// If true show the users username next to the timestamp of the message
|
|
||||||
final bool showUsername;
|
|
||||||
final bool showTimestamp;
|
|
||||||
|
|
||||||
final bool showReplyMessage;
|
|
||||||
final bool showThreadReplyMessage;
|
|
||||||
final bool showEditMessage;
|
|
||||||
final bool showCopyMessage;
|
|
||||||
final bool showDeleteMessage;
|
|
||||||
final bool showResendMessage;
|
|
||||||
|
|
||||||
final bool showFlagButton;
|
|
||||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
|
||||||
|
|
||||||
/// Center user avatar with bottom of the message
|
|
||||||
final bool translateUserAvatar;
|
|
||||||
|
|
||||||
/// Function called when quotedMessage is tapped
|
|
||||||
final OnQuotedMessageTap? onQuotedMessageTap;
|
|
||||||
|
|
||||||
/// Function called when message is tapped
|
|
||||||
final void Function(Message)? onMessageTap;
|
|
||||||
|
|
||||||
/// List of custom actions shown on message long tap
|
|
||||||
final List<MessageAction> customActions;
|
|
||||||
|
|
||||||
// Customize onTap on attachment
|
|
||||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
MessageWidget({
|
MessageWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -201,8 +95,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.readList,
|
this.readList,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.textPadding = const EdgeInsets.symmetric(
|
this.textPadding = const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16,
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
this.allRead = false,
|
||||||
@@ -211,8 +105,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachments) {
|
'image': (context, message, attachments) {
|
||||||
var border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
side: BorderSide.none,
|
|
||||||
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,16 +160,15 @@ class MessageWidget extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
'video': (context, message, attachments) {
|
'video': (context, message, attachments) {
|
||||||
var border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
side: BorderSide.none,
|
|
||||||
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
||||||
);
|
);
|
||||||
|
|
||||||
return wrapAttachmentWidget(
|
return wrapAttachmentWidget(
|
||||||
context,
|
context,
|
||||||
Column(
|
Column(
|
||||||
children: attachments.map((attachment) {
|
children: attachments
|
||||||
return VideoAttachment(
|
.map((attachment) => VideoAttachment(
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
size: Size(
|
size: Size(
|
||||||
@@ -291,8 +183,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
onAttachmentTap(message, attachment);
|
onAttachmentTap(message, attachment);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
))
|
||||||
}).toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
border,
|
border,
|
||||||
reverse,
|
reverse,
|
||||||
@@ -302,15 +194,14 @@ class MessageWidget extends StatefulWidget {
|
|||||||
},
|
},
|
||||||
'giphy': (context, message, attachments) {
|
'giphy': (context, message, attachments) {
|
||||||
final border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
side: BorderSide.none,
|
|
||||||
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
||||||
);
|
);
|
||||||
|
|
||||||
return wrapAttachmentWidget(
|
return wrapAttachmentWidget(
|
||||||
context,
|
context,
|
||||||
Column(
|
Column(
|
||||||
children: attachments.map((attachment) {
|
children: attachments
|
||||||
return GiphyAttachment(
|
.map((attachment) => GiphyAttachment(
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
message: message,
|
message: message,
|
||||||
size: Size(
|
size: Size(
|
||||||
@@ -319,8 +210,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
onShowMessage: onShowMessage,
|
onShowMessage: onShowMessage,
|
||||||
onReturnAction: onReturnAction,
|
onReturnAction: onReturnAction,
|
||||||
);
|
))
|
||||||
}).toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
border,
|
border,
|
||||||
reverse,
|
reverse,
|
||||||
@@ -329,7 +220,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
'file': (context, message, attachments) {
|
'file': (context, message, attachments) {
|
||||||
var border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
side: attachmentBorderSide ??
|
side: attachmentBorderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||||
@@ -339,8 +230,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: attachments
|
children: attachments
|
||||||
.map<Widget>((attachment) {
|
.map<Widget>((attachment) => wrapAttachmentWidget(
|
||||||
return wrapAttachmentWidget(
|
|
||||||
context,
|
context,
|
||||||
FileAttachment(
|
FileAttachment(
|
||||||
message: message,
|
message: message,
|
||||||
@@ -354,8 +244,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
reverse,
|
reverse,
|
||||||
attachmentBorderRadiusGeometry as BorderRadius? ??
|
attachmentBorderRadiusGeometry as BorderRadius? ??
|
||||||
BorderRadius.zero,
|
BorderRadius.zero,
|
||||||
);
|
))
|
||||||
})
|
|
||||||
.insertBetween(SizedBox(
|
.insertBetween(SizedBox(
|
||||||
height: attachmentPadding.vertical / 2,
|
height: attachmentPadding.vertical / 2,
|
||||||
))
|
))
|
||||||
@@ -365,6 +254,141 @@ class MessageWidget extends StatefulWidget {
|
|||||||
}..addAll(customAttachmentBuilders ?? {}),
|
}..addAll(customAttachmentBuilders ?? {}),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
/// Function called on mention tap
|
||||||
|
final void Function(User)? onMentionTap;
|
||||||
|
|
||||||
|
/// The function called when tapping on threads
|
||||||
|
final void Function(Message)? onThreadTap;
|
||||||
|
|
||||||
|
/// The function called when tapping on replies
|
||||||
|
final void Function(Message)? onReplyTap;
|
||||||
|
|
||||||
|
/// Widget builder for edit message layout
|
||||||
|
final Widget Function(BuildContext, Message)? editMessageInputBuilder;
|
||||||
|
|
||||||
|
/// Widget builder for building text
|
||||||
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
|
/// Function called on long press
|
||||||
|
final void Function(BuildContext, Message)? onMessageActions;
|
||||||
|
|
||||||
|
/// The message
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// The message theme
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// If true the widget will be mirrored
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// The shape of the message text
|
||||||
|
final ShapeBorder? shape;
|
||||||
|
|
||||||
|
/// The shape of an attachment
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
|
/// The borderside of the message text
|
||||||
|
final BorderSide? borderSide;
|
||||||
|
|
||||||
|
/// The borderside of an attachment
|
||||||
|
final BorderSide? attachmentBorderSide;
|
||||||
|
|
||||||
|
/// The border radius of the message text
|
||||||
|
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||||
|
|
||||||
|
/// The border radius of an attachment
|
||||||
|
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
|
||||||
|
|
||||||
|
/// The padding of the widget
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// The internal padding of the message text
|
||||||
|
final EdgeInsets textPadding;
|
||||||
|
|
||||||
|
/// The internal padding of an attachment
|
||||||
|
final EdgeInsetsGeometry attachmentPadding;
|
||||||
|
|
||||||
|
/// It controls the display behaviour of the user avatar
|
||||||
|
final DisplayWidget showUserAvatar;
|
||||||
|
|
||||||
|
/// It controls the display behaviour of the sending indicator
|
||||||
|
final bool showSendingIndicator;
|
||||||
|
|
||||||
|
/// If true the widget will show the reactions
|
||||||
|
final bool showReactions;
|
||||||
|
|
||||||
|
///
|
||||||
|
final bool allRead;
|
||||||
|
|
||||||
|
/// If true the widget will show the thread reply indicator
|
||||||
|
final bool showThreadReplyIndicator;
|
||||||
|
|
||||||
|
/// If true the widget will show the show in channel indicator
|
||||||
|
final bool showInChannelIndicator;
|
||||||
|
|
||||||
|
/// The function called when tapping on UserAvatar
|
||||||
|
final void Function(User)? onUserAvatarTap;
|
||||||
|
|
||||||
|
/// The function called when tapping on a link
|
||||||
|
final void Function(String)? onLinkTap;
|
||||||
|
|
||||||
|
/// Used in [MessageReactionsModal] and [MessageActionsModal]
|
||||||
|
final bool showReactionPickerIndicator;
|
||||||
|
|
||||||
|
/// List of users who read
|
||||||
|
final List<Read>? readList;
|
||||||
|
|
||||||
|
/// Callback when show message is tapped
|
||||||
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Handle return actions like reply message
|
||||||
|
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||||
|
|
||||||
|
/// If true show the users username next to the timestamp of the message
|
||||||
|
final bool showUsername;
|
||||||
|
|
||||||
|
/// Show message timestamp
|
||||||
|
final bool showTimestamp;
|
||||||
|
|
||||||
|
/// Show reply action
|
||||||
|
final bool showReplyMessage;
|
||||||
|
|
||||||
|
/// Show thread reply action
|
||||||
|
final bool showThreadReplyMessage;
|
||||||
|
|
||||||
|
/// Show edit action
|
||||||
|
final bool showEditMessage;
|
||||||
|
|
||||||
|
/// Show copy action
|
||||||
|
final bool showCopyMessage;
|
||||||
|
|
||||||
|
/// Show delete action
|
||||||
|
final bool showDeleteMessage;
|
||||||
|
|
||||||
|
/// Show resend action
|
||||||
|
final bool showResendMessage;
|
||||||
|
|
||||||
|
/// Show flag action
|
||||||
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Builder for respective attachment types
|
||||||
|
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||||
|
|
||||||
|
/// Center user avatar with bottom of the message
|
||||||
|
final bool translateUserAvatar;
|
||||||
|
|
||||||
|
/// Function called when quotedMessage is tapped
|
||||||
|
final OnQuotedMessageTap? onQuotedMessageTap;
|
||||||
|
|
||||||
|
/// Function called when message is tapped
|
||||||
|
final void Function(Message)? onMessageTap;
|
||||||
|
|
||||||
|
/// List of custom actions shown on message long tap
|
||||||
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
|
/// Customize onTap on attachment
|
||||||
|
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageWidgetState createState() => _MessageWidgetState();
|
_MessageWidgetState createState() => _MessageWidgetState();
|
||||||
}
|
}
|
||||||
@@ -426,7 +450,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
final avatarWidth =
|
final avatarWidth =
|
||||||
widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40;
|
widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40;
|
||||||
var leftPadding =
|
final leftPadding =
|
||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
@@ -440,7 +464,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
? null
|
? null
|
||||||
: () => onLongPress(context),
|
: () => onLongPress(context),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.padding ?? EdgeInsets.all(8),
|
padding: widget.padding ?? const EdgeInsets.all(8),
|
||||||
child: Transform(
|
child: Transform(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
@@ -460,7 +484,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -468,7 +491,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
DisplayWidget.show &&
|
DisplayWidget.show &&
|
||||||
widget.message.user != null) ...[
|
widget.message.user != null) ...[
|
||||||
_buildUserAvatar(),
|
_buildUserAvatar(),
|
||||||
SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
SizedBox(width: avatarWidth + 4),
|
SizedBox(width: avatarWidth + 4),
|
||||||
@@ -477,12 +500,12 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
portal: Container(
|
portal: Container(
|
||||||
transform:
|
transform:
|
||||||
Matrix4.translationValues(-12, 0, 0),
|
Matrix4.translationValues(-12, 0, 0),
|
||||||
constraints:
|
constraints: const BoxConstraints(
|
||||||
BoxConstraints(maxWidth: 22 * 6.0),
|
maxWidth: 22 * 6.0),
|
||||||
child: _buildReactionIndicator(context),
|
child: _buildReactionIndicator(context),
|
||||||
),
|
),
|
||||||
portalAnchor: Alignment(-1.0, -1.0),
|
portalAnchor: const Alignment(-1, -1),
|
||||||
childAnchor: Alignment(1, -1.0),
|
childAnchor: const Alignment(1, -1),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
@@ -505,15 +528,18 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
transform: Matrix4.rotationY(
|
transform: Matrix4.rotationY(
|
||||||
widget.reverse ? pi : 0),
|
widget.reverse ? pi : 0),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
horizontal:
|
horizontal:
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
widget.showUserAvatar ==
|
widget.showUserAvatar ==
|
||||||
DisplayWidget
|
// ignore: lines_longer_than_80_chars
|
||||||
.gone
|
DisplayWidget.gone
|
||||||
? 0
|
? 0
|
||||||
: 4.0),
|
: 4.0),
|
||||||
child: DeletedMessage(
|
child: DeletedMessage(
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
borderRadiusGeometry: widget
|
borderRadiusGeometry: widget
|
||||||
.borderRadiusGeometry,
|
.borderRadiusGeometry,
|
||||||
borderSide:
|
borderSide:
|
||||||
@@ -526,11 +552,12 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
)
|
)
|
||||||
: Card(
|
: Card(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
elevation: 0.0,
|
elevation: 0,
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
horizontal: (isFailedState
|
horizontal: (isFailedState
|
||||||
? 15.0
|
? 15.0
|
||||||
: 0.0) +
|
: 0.0) +
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
(widget.showUserAvatar ==
|
(widget.showUserAvatar ==
|
||||||
DisplayWidget
|
DisplayWidget
|
||||||
.gone
|
.gone
|
||||||
@@ -543,11 +570,14 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
.borderSide ??
|
.borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: widget
|
color: widget
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
.messageTheme
|
.messageTheme
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
.messageBorderColor ??
|
.messageBorderColor ??
|
||||||
Colors.grey,
|
Colors.grey,
|
||||||
),
|
),
|
||||||
borderRadius: widget
|
borderRadius: widget
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
.borderRadiusGeometry ??
|
.borderRadiusGeometry ??
|
||||||
BorderRadius.zero,
|
BorderRadius.zero,
|
||||||
),
|
),
|
||||||
@@ -650,9 +680,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
children: [
|
children: [
|
||||||
StreamSvgIcon.eye(
|
StreamSvgIcon.eye(
|
||||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
size: 16.0,
|
size: 16,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8.0),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Only visible to you',
|
'Only visible to you',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
@@ -665,7 +695,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var children = <Widget>[];
|
final children = <Widget>[];
|
||||||
|
|
||||||
final threadParticipants = widget.message.threadParticipants?.take(2);
|
final threadParticipants = widget.message.threadParticipants?.take(2);
|
||||||
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
||||||
@@ -676,6 +706,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
msg = '$replyCount Thread Replies';
|
msg = '$replyCount Thread Replies';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
final onThreadTap = () async {
|
final onThreadTap = () async {
|
||||||
try {
|
try {
|
||||||
var message = widget.message;
|
var message = widget.message;
|
||||||
@@ -687,6 +718,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
print(e);
|
print(e);
|
||||||
print(stk);
|
print(stk);
|
||||||
|
// ignore: avoid_returning_null_for_void
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -734,7 +766,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
((widget.messageTheme.replies?.fontSize ?? 1) / 2),
|
((widget.messageTheme.replies?.fontSize ?? 1) / 2),
|
||||||
),
|
),
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
size: Size(16, 32) * context.textScaleFactor,
|
size: const Size(16, 32) * context.textScaleFactor,
|
||||||
painter: _ThreadReplyPainter(
|
painter: _ThreadReplyPainter(
|
||||||
context: context,
|
context: context,
|
||||||
color: widget.messageTheme.messageBorderColor,
|
color: widget.messageTheme.messageBorderColor,
|
||||||
@@ -746,7 +778,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
Widget mappedChild = Transform(
|
Widget mappedChild = Transform(
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: context.textScaleFactor * 14,
|
height: context.textScaleFactor * 14,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
@@ -757,18 +789,18 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
return mappedChild;
|
return mappedChild;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
].insertBetween(const SizedBox(width: 8.0)),
|
].insertBetween(const SizedBox(width: 8)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUrlAttachment() {
|
Widget _buildUrlAttachment() {
|
||||||
var urlAttachment = widget.message.attachments
|
final urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||||
|
|
||||||
var host = Uri.parse(urlAttachment.ogScrapeUrl!).host;
|
final host = Uri.parse(urlAttachment.ogScrapeUrl!).host;
|
||||||
var splitList = host.split('.');
|
final splitList = host.split('.');
|
||||||
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||||
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||||
getWebsiteName(hostName.toLowerCase()) ??
|
getWebsiteName(hostName.toLowerCase()) ??
|
||||||
hostName.capitalize();
|
hostName.capitalize();
|
||||||
|
|
||||||
@@ -796,7 +828,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
padding: const EdgeInsets.all(1),
|
padding: const EdgeInsets.all(1),
|
||||||
child: UserAvatar(
|
child: UserAvatar(
|
||||||
user: user,
|
user: user,
|
||||||
constraints: BoxConstraints.loose(Size.fromRadius(7)),
|
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
|
||||||
showOnlineStatus: false,
|
showOnlineStatus: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -820,7 +852,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
..sort((a, b) => a.user!.id == ownId ? 1 : -1);
|
..sort((a, b) => a.user!.id == ownId ? 1 : -1);
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: (widget.showReactions &&
|
child: (widget.showReactions &&
|
||||||
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
||||||
!widget.message.isDeleted)
|
!widget.message.isDeleted)
|
||||||
@@ -839,7 +871,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
reactions: reactionsList,
|
reactions: reactionsList,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: const SizedBox(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,8 +881,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
onCopyTap: (message) =>
|
onCopyTap: (message) =>
|
||||||
@@ -863,16 +894,16 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
: DisplayWidget.show,
|
: DisplayWidget.show,
|
||||||
messageTheme: widget.messageTheme,
|
messageTheme: widget.messageTheme,
|
||||||
messageShape: widget.shape ?? _getDefaultShape(context),
|
messageShape: widget.shape ?? _getDefaultShape(context),
|
||||||
attachmentShape:
|
attachmentShape: widget.attachmentShape ??
|
||||||
widget.attachmentShape ?? _getDefaultAttachmentShape(context),
|
_getDefaultAttachmentShape(context),
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||||
onReplyTap: widget.onReplyTap,
|
onReplyTap: widget.onReplyTap,
|
||||||
onThreadReplyTap: widget.onThreadTap,
|
onThreadReplyTap: widget.onThreadTap,
|
||||||
showResendMessage:
|
showResendMessage: widget.showResendMessage &&
|
||||||
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
(isSendFailed || isUpdateFailed),
|
||||||
showCopyMessage: widget.showCopyMessage &&
|
showCopyMessage: widget.showCopyMessage &&
|
||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.message.text?.trim().isNotEmpty == true,
|
widget.message.text?.trim().isNotEmpty == true,
|
||||||
@@ -891,8 +922,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
customActions: widget.customActions,
|
customActions: widget.customActions,
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMessageReactionsModalBottomSheet(BuildContext context) {
|
void _showMessageReactionsModalBottomSheet(BuildContext context) {
|
||||||
@@ -900,8 +930,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||||
builder: (context) {
|
builder: (context) => StreamChannel(
|
||||||
return StreamChannel(
|
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageReactionsModal(
|
child: MessageReactionsModal(
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
@@ -919,8 +948,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
message: widget.message,
|
message: widget.message,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeBorder _getDefaultAttachmentShape(BuildContext context) {
|
ShapeBorder _getDefaultAttachmentShape(BuildContext context) {
|
||||||
@@ -937,15 +966,13 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
||||||
return RoundedRectangleBorder(
|
|
||||||
side: widget.borderSide ??
|
side: widget.borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||||
),
|
),
|
||||||
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _parseAttachments() {
|
Widget _parseAttachments() {
|
||||||
final attachmentGroups = <String, List<Attachment>>{};
|
final attachmentGroups = <String, List<Attachment>>{};
|
||||||
@@ -1007,9 +1034,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
(message.status == MessageSendingStatus.sending ||
|
(message.status == MessageSendingStatus.sending ||
|
||||||
message.status == MessageSendingStatus.updating)) {
|
message.status == MessageSendingStatus.updating)) {
|
||||||
final totalAttachments = message.attachments.length;
|
final totalAttachments = message.attachments.length;
|
||||||
final uploadRemaining = message.attachments.where((it) {
|
final uploadRemaining =
|
||||||
return !it.uploadState.isSuccess;
|
message.attachments.where((it) => !it.uploadState.isSuccess).length;
|
||||||
}).length;
|
|
||||||
if (uploadRemaining == 0) {
|
if (uploadRemaining == 0) {
|
||||||
return StreamSvgIcon.check(
|
return StreamSvgIcon.check(
|
||||||
size: style!.fontSize,
|
size: style!.fontSize,
|
||||||
@@ -1037,7 +1063,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
child,
|
child,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1068,7 +1094,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTextBubble() {
|
Widget _buildTextBubble() {
|
||||||
if (widget.message.text!.trim().isEmpty) return Offstage();
|
if (widget.message.text!.trim().isEmpty) return const Offstage();
|
||||||
return Transform(
|
return Transform(
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -1139,11 +1165,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ThreadReplyPainter extends CustomPainter {
|
class _ThreadReplyPainter extends CustomPainter {
|
||||||
|
const _ThreadReplyPainter({this.context, required this.color});
|
||||||
|
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final BuildContext? context;
|
final BuildContext? context;
|
||||||
|
|
||||||
const _ThreadReplyPainter({this.context, required this.color});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
|
||||||
|
/// List tile for [ChannelBottomSheet]
|
||||||
class OptionListTile extends StatelessWidget {
|
class OptionListTile extends StatelessWidget {
|
||||||
final String? title;
|
/// Constructor for creating [OptionListTile]
|
||||||
final Widget? leading;
|
const OptionListTile({
|
||||||
final Widget? trailing;
|
Key? key,
|
||||||
final VoidCallback? onTap;
|
|
||||||
final Color? titleColor;
|
|
||||||
final Color? tileColor;
|
|
||||||
final Color? separatorColor;
|
|
||||||
final TextStyle? titleTextStyle;
|
|
||||||
|
|
||||||
OptionListTile({
|
|
||||||
this.title,
|
this.title,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.trailing,
|
this.trailing,
|
||||||
@@ -20,29 +14,52 @@ class OptionListTile extends StatelessWidget {
|
|||||||
this.tileColor,
|
this.tileColor,
|
||||||
this.separatorColor,
|
this.separatorColor,
|
||||||
this.titleTextStyle,
|
this.titleTextStyle,
|
||||||
});
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Title for tile
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
/// Leading widget (start)
|
||||||
|
final Widget? leading;
|
||||||
|
|
||||||
|
/// Trailing widget (end)
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
/// Callback when tile is tapped
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
/// Title color
|
||||||
|
final Color? titleColor;
|
||||||
|
|
||||||
|
/// Background tile color
|
||||||
|
final Color? tileColor;
|
||||||
|
|
||||||
|
/// Separator color
|
||||||
|
final Color? separatorColor;
|
||||||
|
|
||||||
|
/// [TextStyle] to apply to [title]
|
||||||
|
final TextStyle? titleTextStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Column(
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: separatorColor ??
|
color: separatorColor ??
|
||||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||||
height: 1.0,
|
height: 1,
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
|
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: 63.0,
|
height: 63,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (leading != null) Center(child: leading),
|
if (leading != null) Center(child: leading),
|
||||||
if (leading == null)
|
if (leading == null)
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 16.0,
|
width: 16,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
@@ -62,7 +79,7 @@ class OptionListTile extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
padding: const EdgeInsets.only(right: 16),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: trailing ?? Container(),
|
child: trailing ?? Container(),
|
||||||
@@ -77,4 +94,3 @@ class OptionListTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,29 +4,25 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
|
||||||
import 'attachment/attachment.dart';
|
/// Widget builder for quoted message attachment thumnail
|
||||||
import 'extension.dart';
|
|
||||||
import 'message_text.dart';
|
|
||||||
import 'stream_chat_theme.dart';
|
|
||||||
import 'user_avatar.dart';
|
|
||||||
import 'utils.dart';
|
|
||||||
|
|
||||||
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
Attachment,
|
Attachment,
|
||||||
);
|
);
|
||||||
|
|
||||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||||
final Size size;
|
|
||||||
final Attachment attachment;
|
|
||||||
|
|
||||||
const _VideoAttachmentThumbnail({
|
const _VideoAttachmentThumbnail({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.attachment,
|
required this.attachment,
|
||||||
this.size = const Size(32, 32),
|
this.size = const Size(32, 32),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Size size;
|
||||||
|
final Attachment attachment;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_VideoAttachmentThumbnailState createState() =>
|
_VideoAttachmentThumbnailState createState() =>
|
||||||
_VideoAttachmentThumbnailState();
|
_VideoAttachmentThumbnailState();
|
||||||
@@ -51,18 +47,30 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SizedBox(
|
||||||
return Container(
|
|
||||||
height: widget.size.height,
|
height: widget.size.height,
|
||||||
width: widget.size.width,
|
width: widget.size.width,
|
||||||
child: _controller.value.isInitialized
|
child: _controller.value.isInitialized
|
||||||
? VideoPlayer(_controller)
|
? VideoPlayer(_controller)
|
||||||
: CircularProgressIndicator());
|
: const CircularProgressIndicator(),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
class QuotedMessageWidget extends StatelessWidget {
|
class QuotedMessageWidget extends StatelessWidget {
|
||||||
|
///
|
||||||
|
const QuotedMessageWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
required this.messageTheme,
|
||||||
|
this.reverse = false,
|
||||||
|
this.showBorder = false,
|
||||||
|
this.textLimit = 170,
|
||||||
|
this.attachmentThumbnailBuilders,
|
||||||
|
this.padding = const EdgeInsets.all(8),
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The message
|
/// The message
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
@@ -82,23 +90,12 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||||
attachmentThumbnailBuilders;
|
attachmentThumbnailBuilders;
|
||||||
|
|
||||||
|
/// Padding around the widget
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
/// Callback for tap on widget
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
///
|
|
||||||
QuotedMessageWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.message,
|
|
||||||
required this.messageTheme,
|
|
||||||
this.reverse = false,
|
|
||||||
this.showBorder = false,
|
|
||||||
this.textLimit = 170,
|
|
||||||
this.attachmentThumbnailBuilders,
|
|
||||||
this.padding = const EdgeInsets.all(8),
|
|
||||||
this.onTap,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
bool get _hasAttachments => message.attachments.isNotEmpty == true;
|
bool get _hasAttachments => message.attachments.isNotEmpty == true;
|
||||||
|
|
||||||
bool get _containsScrapeUrl =>
|
bool get _containsScrapeUrl =>
|
||||||
@@ -107,8 +104,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
bool get _containsText => message.text?.isNotEmpty == true;
|
bool get _containsText => message.text?.isNotEmpty == true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Padding(
|
||||||
return Padding(
|
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -117,13 +113,12 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(child: _buildMessage(context)),
|
Flexible(child: _buildMessage(context)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (message.user != null) _buildUserAvatar(),
|
if (message.user != null) _buildUserAvatar(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMessage(BuildContext context) {
|
Widget _buildMessage(BuildContext context) {
|
||||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||||
@@ -165,7 +160,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(12),
|
topRight: Radius.circular(12),
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
bottomLeft: Radius.circular(12),
|
bottomLeft: Radius.circular(12),
|
||||||
@@ -183,7 +178,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUrlAttachment(Attachment attachment) {
|
Widget _buildUrlAttachment(Attachment attachment) {
|
||||||
final size = Size(32, 32);
|
const size = Size(32, 32);
|
||||||
if (attachment.thumbUrl != null) {
|
if (attachment.thumbUrl != null) {
|
||||||
return Container(
|
return Container(
|
||||||
height: size.height,
|
height: size.height,
|
||||||
@@ -198,7 +193,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return AttachmentError(size: size);
|
return const AttachmentError(size: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _parseAttachments(BuildContext context) {
|
Widget _parseAttachments(BuildContext context) {
|
||||||
@@ -217,7 +212,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
|
attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
|
||||||
if (attachmentBuilder == null) {
|
if (attachmentBuilder == null) {
|
||||||
child = Offstage();
|
child = const Offstage();
|
||||||
} else {
|
} else {
|
||||||
child = attachmentBuilder(context, attachment);
|
child = attachmentBuilder(context, attachment);
|
||||||
}
|
}
|
||||||
@@ -235,77 +230,63 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
||||||
return RoundedRectangleBorder(
|
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||||
side: BorderSide(width: 0.0, color: Colors.transparent),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildUserAvatar() {
|
Widget _buildUserAvatar() => Transform(
|
||||||
return Transform(
|
|
||||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: UserAvatar(
|
child: UserAvatar(
|
||||||
user: message.user!,
|
user: message.user!,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
showOnlineStatus: false,
|
showOnlineStatus: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||||
get _defaultAttachmentBuilder {
|
get _defaultAttachmentBuilder => {
|
||||||
return {
|
'image': (_, attachment) => ImageAttachment(
|
||||||
'image': (_, attachment) {
|
|
||||||
return ImageAttachment(
|
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
message: message,
|
message: message,
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
size: Size(32, 32),
|
size: const Size(32, 32),
|
||||||
);
|
),
|
||||||
},
|
'video': (_, attachment) => _VideoAttachmentThumbnail(
|
||||||
'video': (_, attachment) {
|
|
||||||
return _VideoAttachmentThumbnail(
|
|
||||||
key: ValueKey(attachment.assetUrl),
|
key: ValueKey(attachment.assetUrl),
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
'giphy': (_, attachment) {
|
'giphy': (_, attachment) {
|
||||||
final size = Size(32, 32);
|
const size = Size(32, 32);
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
height: size.height,
|
height: size.height,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) => SizedBox(
|
||||||
return Container(
|
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
imageUrl: attachment.thumbUrl ??
|
imageUrl: attachment.thumbUrl ??
|
||||||
attachment.imageUrl ??
|
attachment.imageUrl ??
|
||||||
attachment.assetUrl!,
|
attachment.assetUrl!,
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) =>
|
||||||
return AttachmentError(size: size);
|
const AttachmentError(size: size),
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'file': (_, attachment) {
|
'file': (_, attachment) => SizedBox(
|
||||||
return Container(
|
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
child: getFileTypeImage(attachment.extraData['mime_type'] as String?),
|
child: getFileTypeImage(
|
||||||
);
|
attachment.extraData['mime_type'] as String?),
|
||||||
},
|
),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Color? _getBackgroundColor(BuildContext context) {
|
Color? _getBackgroundColor(BuildContext context) {
|
||||||
if (_containsScrapeUrl) {
|
if (_containsScrapeUrl) {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Creates reaction bubble widget for displaying over messages
|
||||||
class ReactionBubble extends StatelessWidget {
|
class ReactionBubble extends StatelessWidget {
|
||||||
|
/// Constructor for creating a [ReactionBubble]
|
||||||
const ReactionBubble({
|
const ReactionBubble({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.reactions,
|
required this.reactions,
|
||||||
@@ -21,13 +23,28 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
this.tailCirclesSpacing = 0,
|
this.tailCirclesSpacing = 0,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Reactions to show
|
||||||
final List<Reaction> reactions;
|
final List<Reaction> reactions;
|
||||||
|
|
||||||
|
/// Border color of bubble
|
||||||
final Color borderColor;
|
final Color borderColor;
|
||||||
|
|
||||||
|
/// Background color of bubble
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// Mask color
|
||||||
final Color maskColor;
|
final Color maskColor;
|
||||||
|
|
||||||
|
/// Reverse for other side
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
|
/// Reverse tail for other side
|
||||||
final bool flipTail;
|
final bool flipTail;
|
||||||
|
|
||||||
|
/// Flag for highlighting own reactions
|
||||||
final bool highlightOwnReactions;
|
final bool highlightOwnReactions;
|
||||||
|
|
||||||
|
/// Spacing for tail circles
|
||||||
final double tailCirclesSpacing;
|
final double tailCirclesSpacing;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,7 +57,6 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
fit: StackFit.loose,
|
|
||||||
children: [
|
children: [
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: Offset(reverse ? offset : -offset, 0),
|
offset: Offset(reverse ? offset : -offset, 0),
|
||||||
@@ -48,7 +64,7 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: maskColor,
|
color: maskColor,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
@@ -60,35 +76,32 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
color: borderColor,
|
color: borderColor,
|
||||||
),
|
),
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(14)),
|
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) => Flex(
|
||||||
return Flex(
|
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (constraints.maxWidth < double.infinity)
|
if (constraints.maxWidth < double.infinity)
|
||||||
...reactions
|
...reactions
|
||||||
.take((constraints.maxWidth) ~/ 24)
|
.take((constraints.maxWidth) ~/ 24)
|
||||||
.map((reaction) {
|
.map((reaction) => _buildReaction(
|
||||||
return _buildReaction(
|
|
||||||
reactionIcons,
|
reactionIcons,
|
||||||
reaction,
|
reaction,
|
||||||
context,
|
context,
|
||||||
);
|
))
|
||||||
}).toList(),
|
.toList(),
|
||||||
if (constraints.maxWidth == double.infinity)
|
if (constraints.maxWidth == double.infinity)
|
||||||
...reactions.map((reaction) {
|
...reactions
|
||||||
return _buildReaction(
|
.map((reaction) => _buildReaction(
|
||||||
reactionIcons,
|
reactionIcons,
|
||||||
reaction,
|
reaction,
|
||||||
context,
|
context,
|
||||||
);
|
))
|
||||||
}).toList(),
|
.toList(),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -115,7 +128,7 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 4.0,
|
horizontal: 4,
|
||||||
),
|
),
|
||||||
child: reactionIcon != null
|
child: reactionIcon != null
|
||||||
? StreamSvgIcon(
|
? StreamSvgIcon(
|
||||||
@@ -161,12 +174,9 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Painter widget for a reaction bubble
|
||||||
class ReactionBubblePainter extends CustomPainter {
|
class ReactionBubblePainter extends CustomPainter {
|
||||||
final Color color;
|
/// Constructor for creating a [ReactionBubblePainter]
|
||||||
final Color borderColor;
|
|
||||||
final Color maskColor;
|
|
||||||
final double tailCirclesSpace;
|
|
||||||
|
|
||||||
ReactionBubblePainter(
|
ReactionBubblePainter(
|
||||||
this.color,
|
this.color,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
@@ -174,6 +184,18 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
this.tailCirclesSpace = 0,
|
this.tailCirclesSpace = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Color of bubble
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// Border color of bubble
|
||||||
|
final Color borderColor;
|
||||||
|
|
||||||
|
/// Mask color
|
||||||
|
final Color maskColor;
|
||||||
|
|
||||||
|
/// Tail circle space
|
||||||
|
final double tailCirclesSpace;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
_drawOvalMask(size, canvas);
|
_drawOvalMask(size, canvas);
|
||||||
@@ -194,10 +216,11 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..color = maskColor
|
..color = maskColor
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addOval(
|
..addOval(
|
||||||
Rect.fromCircle(
|
Rect.fromCircle(
|
||||||
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
center:
|
||||||
|
const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
||||||
radius: 4,
|
radius: 4,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -210,10 +233,11 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..strokeWidth = 1
|
..strokeWidth = 1
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addOval(
|
..addOval(
|
||||||
Rect.fromCircle(
|
Rect.fromCircle(
|
||||||
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
center:
|
||||||
|
const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
||||||
radius: 2,
|
radius: 2,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -225,9 +249,9 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..color = color
|
..color = color
|
||||||
..strokeWidth = 1;
|
..strokeWidth = 1;
|
||||||
|
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addOval(Rect.fromCircle(
|
..addOval(Rect.fromCircle(
|
||||||
center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
center: const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace),
|
||||||
radius: 2,
|
radius: 2,
|
||||||
));
|
));
|
||||||
canvas.drawPath(path, paint);
|
canvas.drawPath(path, paint);
|
||||||
@@ -239,13 +263,13 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..strokeWidth = 1
|
..strokeWidth = 1
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
final dy = -2.2;
|
const dy = -2.2;
|
||||||
final startAngle = 1.1;
|
const startAngle = 1.1;
|
||||||
final sweepAngle = 1.2;
|
const sweepAngle = 1.2;
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addArc(
|
..addArc(
|
||||||
Rect.fromCircle(
|
Rect.fromCircle(
|
||||||
center: Offset(1, dy),
|
center: const Offset(1, dy),
|
||||||
radius: 4,
|
radius: 4,
|
||||||
),
|
),
|
||||||
-pi * startAngle,
|
-pi * startAngle,
|
||||||
@@ -259,13 +283,13 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..color = color
|
..color = color
|
||||||
..strokeWidth = 1;
|
..strokeWidth = 1;
|
||||||
|
|
||||||
final dy = -2.2;
|
const dy = -2.2;
|
||||||
final startAngle = 1;
|
const startAngle = 1;
|
||||||
final sweepAngle = 1.3;
|
const sweepAngle = 1.3;
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addArc(
|
..addArc(
|
||||||
Rect.fromCircle(
|
Rect.fromCircle(
|
||||||
center: Offset(1, dy),
|
center: const Offset(1, dy),
|
||||||
radius: 4,
|
radius: 4,
|
||||||
),
|
),
|
||||||
-pi * startAngle,
|
-pi * startAngle,
|
||||||
@@ -280,13 +304,13 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
..strokeWidth = 1
|
..strokeWidth = 1
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
final dy = -2.2;
|
const dy = -2.2;
|
||||||
final startAngle = 1.1;
|
const startAngle = 1.1;
|
||||||
final sweepAngle = 1.2;
|
const sweepAngle = 1.2;
|
||||||
final path = Path();
|
final path = Path()
|
||||||
path.addArc(
|
..addArc(
|
||||||
Rect.fromCircle(
|
Rect.fromCircle(
|
||||||
center: Offset(1, dy),
|
center: const Offset(1, dy),
|
||||||
radius: 6,
|
radius: 6,
|
||||||
),
|
),
|
||||||
-pi * startAngle,
|
-pi * startAngle,
|
||||||
@@ -296,7 +320,5 @@ class ReactionBubblePainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
/// Reaction icon data
|
||||||
class ReactionIcon {
|
class ReactionIcon {
|
||||||
final String type;
|
/// Constructor for creating [ReactionIcon]
|
||||||
final String assetName;
|
|
||||||
|
|
||||||
ReactionIcon({
|
ReactionIcon({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.assetName,
|
required this.assetName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Type of reaction
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
/// Asset to display for reaction
|
||||||
|
final String assetName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ import 'dart:math';
|
|||||||
import 'package:ezanimation/ezanimation.dart';
|
import 'package:ezanimation/ezanimation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import '../stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'extension.dart';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
/// It shows a reaction picker
|
/// It shows a reaction picker
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions].
|
/// Usually you don't use this widget as it's one of the default widgets used
|
||||||
|
/// by [MessageWidget.onMessageActions].
|
||||||
class ReactionPicker extends StatefulWidget {
|
class ReactionPicker extends StatefulWidget {
|
||||||
|
/// Constructor for creating a [ReactionPicker] widget
|
||||||
const ReactionPicker({
|
const ReactionPicker({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Message to attach the reaction to
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,7 +40,7 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
animations.add(
|
animations.add(
|
||||||
EzAnimation.tween(
|
EzAnimation.tween(
|
||||||
Tween(begin: 0.0, end: 1.0),
|
Tween(begin: 0.0, end: 1.0),
|
||||||
Duration(milliseconds: 500),
|
const Duration(milliseconds: 500),
|
||||||
curve: Curves.easeInOutBack,
|
curve: Curves.easeInOutBack,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -49,11 +50,10 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return TweenAnimationBuilder<double>(
|
return TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0.0, end: 1.0),
|
tween: Tween(begin: 0, end: 1),
|
||||||
curve: Curves.easeInOutBack,
|
curve: Curves.easeInOutBack,
|
||||||
duration: Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
builder: (context, val, wid) {
|
builder: (context, val, wid) => Transform.scale(
|
||||||
return Transform.scale(
|
|
||||||
scale: val,
|
scale: val,
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
@@ -61,8 +61,8 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16,
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -74,21 +74,19 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
?.indexWhere((reaction) =>
|
?.indexWhere((reaction) =>
|
||||||
reaction.type == reactionIcon.type) ??
|
reaction.type == reactionIcon.type) ??
|
||||||
-1;
|
-1;
|
||||||
var index = reactionIcons.indexOf(reactionIcon);
|
final index = reactionIcons.indexOf(reactionIcon);
|
||||||
|
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
shape: ContinuousRectangleBorder(
|
shape: ContinuousRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
@@ -96,8 +94,8 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
if (ownReactionIndex != -1) {
|
if (ownReactionIndex != -1) {
|
||||||
removeReaction(
|
removeReaction(
|
||||||
context,
|
context,
|
||||||
widget
|
widget.message
|
||||||
.message.ownReactions![ownReactionIndex],
|
.ownReactions![ownReactionIndex],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
sendReaction(
|
sendReaction(
|
||||||
@@ -108,9 +106,7 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
},
|
},
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: animations[index],
|
animation: animations[index],
|
||||||
builder: (context, val) {
|
builder: (context, val) => Transform.scale(
|
||||||
return Transform.scale(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
scale: animations[index].value,
|
scale: animations[index].value,
|
||||||
child: StreamSvgIcon(
|
child: StreamSvgIcon(
|
||||||
assetName: reactionIcon.assetName,
|
assetName: reactionIcon.assetName,
|
||||||
@@ -131,31 +127,29 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
.color!
|
.color!
|
||||||
.withOpacity(.5),
|
.withOpacity(.5),
|
||||||
),
|
),
|
||||||
);
|
)),
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.insertBetween(SizedBox(
|
.insertBetween(const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void triggerAnimations() async {
|
void triggerAnimations() async {
|
||||||
for (var a in animations) {
|
for (final a in animations) {
|
||||||
a.start();
|
a.start();
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() async {
|
void pop() async {
|
||||||
for (var a in animations) {
|
for (final a in animations) {
|
||||||
a.stop();
|
a.stop();
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -179,7 +173,7 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (var a in animations) {
|
for (final a in animations) {
|
||||||
a.dispose();
|
a.dispose();
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
|
|
||||||
/// Used to show the sending status of the message
|
/// Used to show the sending status of the message
|
||||||
class SendingIndicator extends StatelessWidget {
|
class SendingIndicator extends StatelessWidget {
|
||||||
final Message message;
|
/// Constructor for creating a [SendingIndicator] widget
|
||||||
final bool isMessageRead;
|
|
||||||
final double? size;
|
|
||||||
|
|
||||||
const SendingIndicator({
|
const SendingIndicator({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
@@ -14,6 +11,15 @@ class SendingIndicator extends StatelessWidget {
|
|||||||
this.size = 12,
|
this.size = 12,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Message for sending indicator
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// Flag if message is read
|
||||||
|
final bool isMessageRead;
|
||||||
|
|
||||||
|
/// Size for message
|
||||||
|
final double? size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isMessageRead) {
|
if (isMessageRead) {
|
||||||
@@ -35,6 +41,6 @@ class SendingIndicator extends StatelessWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,28 +31,33 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
///
|
///
|
||||||
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
|
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
|
||||||
class StreamChat extends StatefulWidget {
|
class StreamChat extends StatefulWidget {
|
||||||
final StreamChatClient client;
|
/// Constructor for creating a [StreamChat] widget
|
||||||
final Widget? child;
|
const StreamChat({
|
||||||
final StreamChatThemeData? streamChatThemeData;
|
|
||||||
|
|
||||||
/// The amount of time that will pass before disconnecting the client in the background
|
|
||||||
final Duration backgroundKeepAlive;
|
|
||||||
|
|
||||||
/// Handler called whenever the [client] receives a new [Event] while the app
|
|
||||||
/// is in background. Can be used to display various notifications depending
|
|
||||||
/// upon the [Event.type]
|
|
||||||
final EventHandler? onBackgroundEventReceived;
|
|
||||||
|
|
||||||
StreamChat({
|
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.client,
|
required this.client,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.streamChatThemeData,
|
this.streamChatThemeData,
|
||||||
this.onBackgroundEventReceived,
|
this.onBackgroundEventReceived,
|
||||||
this.backgroundKeepAlive = const Duration(minutes: 1),
|
this.backgroundKeepAlive = const Duration(minutes: 1),
|
||||||
}) : super(
|
}) : super(key: key);
|
||||||
key: key,
|
|
||||||
);
|
/// Client to do chat ops with
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
/// Child which inherits details
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
/// Theme to pass on
|
||||||
|
final StreamChatThemeData? streamChatThemeData;
|
||||||
|
|
||||||
|
/// The amount of time that will pass before disconnecting the client
|
||||||
|
/// in the background
|
||||||
|
final Duration backgroundKeepAlive;
|
||||||
|
|
||||||
|
/// Handler called whenever the [client] receives a new [Event] while the app
|
||||||
|
/// is in background. Can be used to display various notifications depending
|
||||||
|
/// upon the [Event.type]
|
||||||
|
final EventHandler? onBackgroundEventReceived;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
StreamChatState createState() => StreamChatState();
|
StreamChatState createState() => StreamChatState();
|
||||||
@@ -74,6 +79,7 @@ class StreamChat extends StatefulWidget {
|
|||||||
|
|
||||||
/// The current state of the StreamChat widget
|
/// The current state of the StreamChat widget
|
||||||
class StreamChatState extends State<StreamChat> {
|
class StreamChatState extends State<StreamChat> {
|
||||||
|
/// Gets client from widget
|
||||||
StreamChatClient get client => widget.client;
|
StreamChatClient get client => widget.client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -96,7 +102,7 @@ class StreamChatState extends State<StreamChat> {
|
|||||||
client: client,
|
client: client,
|
||||||
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
||||||
backgroundKeepAlive: widget.backgroundKeepAlive,
|
backgroundKeepAlive: widget.backgroundKeepAlive,
|
||||||
child: widget.child ?? Offstage(),
|
child: widget.child ?? const Offstage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
|
|
||||||
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
||||||
class StreamChatTheme extends InheritedWidget {
|
class StreamChatTheme extends InheritedWidget {
|
||||||
final StreamChatThemeData data;
|
/// Constructor for creating a [StreamChatTheme]
|
||||||
|
const StreamChatTheme({
|
||||||
StreamChatTheme({
|
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.data,
|
required this.data,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
@@ -22,10 +21,11 @@ class StreamChatTheme extends InheritedWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Theme data
|
||||||
|
final StreamChatThemeData data;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(StreamChatTheme old) {
|
bool updateShouldNotify(StreamChatTheme old) => data != old.data;
|
||||||
return data != old.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use this method to get the current [StreamChatThemeData] instance
|
/// Use this method to get the current [StreamChatThemeData] instance
|
||||||
static StreamChatThemeData of(BuildContext context) {
|
static StreamChatThemeData of(BuildContext context) {
|
||||||
@@ -43,42 +43,6 @@ class StreamChatTheme extends InheritedWidget {
|
|||||||
|
|
||||||
/// Theme data
|
/// Theme data
|
||||||
class StreamChatThemeData {
|
class StreamChatThemeData {
|
||||||
/// The text themes used in the widgets
|
|
||||||
final TextTheme textTheme;
|
|
||||||
|
|
||||||
/// The color themes used in the widgets
|
|
||||||
final ColorTheme colorTheme;
|
|
||||||
|
|
||||||
/// Theme of the [ChannelPreview]
|
|
||||||
final ChannelPreviewTheme channelPreviewTheme;
|
|
||||||
|
|
||||||
/// Theme of the [ChannelListHeader]
|
|
||||||
final ChannelListHeaderTheme channelListHeaderTheme;
|
|
||||||
|
|
||||||
/// Theme of the chat widgets dedicated to a channel
|
|
||||||
final ChannelTheme channelTheme;
|
|
||||||
|
|
||||||
/// Theme of the current user messages
|
|
||||||
final MessageTheme ownMessageTheme;
|
|
||||||
|
|
||||||
/// Theme of other users messages
|
|
||||||
final MessageTheme otherMessageTheme;
|
|
||||||
|
|
||||||
/// Theme dedicated to the [MessageInput] widget
|
|
||||||
final MessageInputTheme messageInputTheme;
|
|
||||||
|
|
||||||
/// The widget that will be built when the channel image is unavailable
|
|
||||||
final Widget Function(BuildContext, Channel) defaultChannelImage;
|
|
||||||
|
|
||||||
/// The widget that will be built when the user image is unavailable
|
|
||||||
final Widget Function(BuildContext, User) defaultUserImage;
|
|
||||||
|
|
||||||
/// Primary icon theme
|
|
||||||
final IconThemeData primaryIconTheme;
|
|
||||||
|
|
||||||
/// Assets used for rendering reactions
|
|
||||||
final List<ReactionIcon> reactionIcons;
|
|
||||||
|
|
||||||
/// Create a theme from scratch
|
/// Create a theme from scratch
|
||||||
factory StreamChatThemeData({
|
factory StreamChatThemeData({
|
||||||
Brightness? brightness,
|
Brightness? brightness,
|
||||||
@@ -121,12 +85,15 @@ class StreamChatThemeData {
|
|||||||
return defaultData.merge(customizedData);
|
return defaultData.merge(customizedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Theme initialised with light
|
||||||
factory StreamChatThemeData.light() =>
|
factory StreamChatThemeData.light() =>
|
||||||
StreamChatThemeData(brightness: Brightness.light);
|
StreamChatThemeData(brightness: Brightness.light);
|
||||||
|
|
||||||
|
/// Theme initialised with dark
|
||||||
factory StreamChatThemeData.dark() =>
|
factory StreamChatThemeData.dark() =>
|
||||||
StreamChatThemeData(brightness: Brightness.dark);
|
StreamChatThemeData(brightness: Brightness.dark);
|
||||||
|
|
||||||
|
/// Raw theme init
|
||||||
const StreamChatThemeData.raw({
|
const StreamChatThemeData.raw({
|
||||||
required this.textTheme,
|
required this.textTheme,
|
||||||
required this.colorTheme,
|
required this.colorTheme,
|
||||||
@@ -154,7 +121,44 @@ class StreamChatThemeData {
|
|||||||
return defaultTheme.merge(customizedTheme);
|
return defaultTheme.merge(customizedTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
|
/// The text themes used in the widgets
|
||||||
|
final TextTheme textTheme;
|
||||||
|
|
||||||
|
/// The color themes used in the widgets
|
||||||
|
final ColorTheme colorTheme;
|
||||||
|
|
||||||
|
/// Theme of the [ChannelPreview]
|
||||||
|
final ChannelPreviewTheme channelPreviewTheme;
|
||||||
|
|
||||||
|
/// Theme of the [ChannelListHeader]
|
||||||
|
final ChannelListHeaderTheme channelListHeaderTheme;
|
||||||
|
|
||||||
|
/// Theme of the chat widgets dedicated to a channel
|
||||||
|
final ChannelTheme channelTheme;
|
||||||
|
|
||||||
|
/// Theme of the current user messages
|
||||||
|
final MessageTheme ownMessageTheme;
|
||||||
|
|
||||||
|
/// Theme of other users messages
|
||||||
|
final MessageTheme otherMessageTheme;
|
||||||
|
|
||||||
|
/// Theme dedicated to the [MessageInput] widget
|
||||||
|
final MessageInputTheme messageInputTheme;
|
||||||
|
|
||||||
|
/// The widget that will be built when the channel image is unavailable
|
||||||
|
final Widget Function(BuildContext, Channel) defaultChannelImage;
|
||||||
|
|
||||||
|
/// The widget that will be built when the user image is unavailable
|
||||||
|
final Widget Function(BuildContext, User) defaultUserImage;
|
||||||
|
|
||||||
|
/// Primary icon theme
|
||||||
|
final IconThemeData primaryIconTheme;
|
||||||
|
|
||||||
|
/// Assets used for rendering reactions
|
||||||
|
final List<ReactionIcon> reactionIcons;
|
||||||
|
|
||||||
|
/// Creates a copy of [StreamChatThemeData] with specified attributes
|
||||||
|
/// overridden.
|
||||||
StreamChatThemeData copyWith({
|
StreamChatThemeData copyWith({
|
||||||
TextTheme? textTheme,
|
TextTheme? textTheme,
|
||||||
ColorTheme? colorTheme,
|
ColorTheme? colorTheme,
|
||||||
@@ -185,6 +189,7 @@ class StreamChatThemeData {
|
|||||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge themes
|
||||||
StreamChatThemeData merge(StreamChatThemeData? other) {
|
StreamChatThemeData merge(StreamChatThemeData? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -204,6 +209,8 @@ class StreamChatThemeData {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create theme from color and text theme
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static StreamChatThemeData fromColorAndTextTheme(
|
static StreamChatThemeData fromColorAndTextTheme(
|
||||||
ColorTheme colorTheme,
|
ColorTheme colorTheme,
|
||||||
TextTheme textTheme,
|
TextTheme textTheme,
|
||||||
@@ -213,7 +220,7 @@ class StreamChatThemeData {
|
|||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
colorTheme: colorTheme,
|
colorTheme: colorTheme,
|
||||||
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
|
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
|
||||||
defaultChannelImage: (context, channel) => SizedBox(),
|
defaultChannelImage: (context, channel) => const SizedBox(),
|
||||||
defaultUserImage: (context, user) => Center(
|
defaultUserImage: (context, user) => Center(
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
@@ -225,23 +232,23 @@ class StreamChatThemeData {
|
|||||||
unreadCounterColor: colorTheme.accentRed,
|
unreadCounterColor: colorTheme.accentRed,
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: textTheme.bodyBold,
|
title: textTheme.bodyBold,
|
||||||
subtitle: textTheme.footnote.copyWith(
|
subtitle: textTheme.footnote.copyWith(
|
||||||
color: Color(0xff7A7A7A),
|
color: const Color(0xff7A7A7A),
|
||||||
),
|
),
|
||||||
lastMessageAt: textTheme.footnote.copyWith(
|
lastMessageAt: textTheme.footnote.copyWith(
|
||||||
color: colorTheme.black.withOpacity(.5),
|
color: colorTheme.black.withOpacity(.5),
|
||||||
),
|
),
|
||||||
indicatorIconSize: 16.0),
|
indicatorIconSize: 16),
|
||||||
channelListHeaderTheme: ChannelListHeaderTheme(
|
channelListHeaderTheme: ChannelListHeaderTheme(
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
@@ -253,7 +260,7 @@ class StreamChatThemeData {
|
|||||||
channelHeaderTheme: ChannelHeaderTheme(
|
channelHeaderTheme: ChannelHeaderTheme(
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
@@ -261,7 +268,7 @@ class StreamChatThemeData {
|
|||||||
color: colorTheme.white,
|
color: colorTheme.white,
|
||||||
title: textTheme.headlineBold,
|
title: textTheme.headlineBold,
|
||||||
subtitle: textTheme.footnote.copyWith(
|
subtitle: textTheme.footnote.copyWith(
|
||||||
color: Color(0xff7A7A7A),
|
color: const Color(0xff7A7A7A),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -277,7 +284,7 @@ class StreamChatThemeData {
|
|||||||
messageBorderColor: colorTheme.greyGainsboro,
|
messageBorderColor: colorTheme.greyGainsboro,
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
),
|
),
|
||||||
@@ -301,7 +308,7 @@ class StreamChatThemeData {
|
|||||||
messageBorderColor: colorTheme.greyWhisper,
|
messageBorderColor: colorTheme.greyWhisper,
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
),
|
),
|
||||||
@@ -309,7 +316,7 @@ class StreamChatThemeData {
|
|||||||
),
|
),
|
||||||
messageInputTheme: MessageInputTheme(
|
messageInputTheme: MessageInputTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
sendAnimationDuration: Duration(milliseconds: 300),
|
sendAnimationDuration: const Duration(milliseconds: 300),
|
||||||
actionButtonColor: colorTheme.accentBlue,
|
actionButtonColor: colorTheme.accentBlue,
|
||||||
actionButtonIdleColor: colorTheme.grey,
|
actionButtonIdleColor: colorTheme.grey,
|
||||||
expandButtonColor: colorTheme.accentBlue,
|
expandButtonColor: colorTheme.accentBlue,
|
||||||
@@ -356,16 +363,9 @@ class StreamChatThemeData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for holding text theme
|
||||||
class TextTheme {
|
class TextTheme {
|
||||||
final TextStyle title;
|
/// Initialise light text theme
|
||||||
final TextStyle headlineBold;
|
|
||||||
final TextStyle headline;
|
|
||||||
final TextStyle bodyBold;
|
|
||||||
final TextStyle body;
|
|
||||||
final TextStyle footnoteBold;
|
|
||||||
final TextStyle footnote;
|
|
||||||
final TextStyle captionBold;
|
|
||||||
|
|
||||||
TextTheme.light({
|
TextTheme.light({
|
||||||
this.title = const TextStyle(
|
this.title = const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
@@ -408,6 +408,7 @@ class TextTheme {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Initialise with dark theme
|
||||||
TextTheme.dark({
|
TextTheme.dark({
|
||||||
this.title = const TextStyle(
|
this.title = const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
@@ -450,6 +451,31 @@ class TextTheme {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Text theme for title
|
||||||
|
final TextStyle title;
|
||||||
|
|
||||||
|
/// Body Text theme for headline
|
||||||
|
final TextStyle headlineBold;
|
||||||
|
|
||||||
|
/// Text theme for headline
|
||||||
|
final TextStyle headline;
|
||||||
|
|
||||||
|
/// Bold Text theme for body
|
||||||
|
final TextStyle bodyBold;
|
||||||
|
|
||||||
|
/// Text theme body
|
||||||
|
final TextStyle body;
|
||||||
|
|
||||||
|
/// Bold Text theme for footnote
|
||||||
|
final TextStyle footnoteBold;
|
||||||
|
|
||||||
|
/// Text theme for footnote
|
||||||
|
final TextStyle footnote;
|
||||||
|
|
||||||
|
/// Bold Text theme for caption
|
||||||
|
final TextStyle captionBold;
|
||||||
|
|
||||||
|
/// Copy with theme
|
||||||
TextTheme copyWith({
|
TextTheme copyWith({
|
||||||
Brightness brightness = Brightness.light,
|
Brightness brightness = Brightness.light,
|
||||||
TextStyle? body,
|
TextStyle? body,
|
||||||
@@ -460,8 +486,8 @@ class TextTheme {
|
|||||||
TextStyle? footnoteBold,
|
TextStyle? footnoteBold,
|
||||||
TextStyle? footnote,
|
TextStyle? footnote,
|
||||||
TextStyle? captionBold,
|
TextStyle? captionBold,
|
||||||
}) {
|
}) =>
|
||||||
return brightness == Brightness.light
|
brightness == Brightness.light
|
||||||
? TextTheme.light(
|
? TextTheme.light(
|
||||||
body: body ?? this.body,
|
body: body ?? this.body,
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
@@ -482,8 +508,8 @@ class TextTheme {
|
|||||||
footnote: footnote ?? this.footnote,
|
footnote: footnote ?? this.footnote,
|
||||||
captionBold: captionBold ?? this.captionBold,
|
captionBold: captionBold ?? this.captionBold,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Merge text theme
|
||||||
TextTheme merge(TextTheme? other) {
|
TextTheme merge(TextTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -499,28 +525,9 @@ class TextTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Theme that holds colors
|
||||||
class ColorTheme {
|
class ColorTheme {
|
||||||
final Color black;
|
/// Initialise with light theme
|
||||||
final Color grey;
|
|
||||||
final Color greyGainsboro;
|
|
||||||
final Color greyWhisper;
|
|
||||||
final Color whiteSmoke;
|
|
||||||
final Color whiteSnow;
|
|
||||||
final Color white;
|
|
||||||
final Color blueAlice;
|
|
||||||
final Color accentBlue;
|
|
||||||
final Color accentRed;
|
|
||||||
final Color accentGreen;
|
|
||||||
final Effect borderTop;
|
|
||||||
final Effect borderBottom;
|
|
||||||
final Effect shadowIconButton;
|
|
||||||
final Effect modalShadow;
|
|
||||||
final Color highlight;
|
|
||||||
final Color overlay;
|
|
||||||
final Color overlayDark;
|
|
||||||
final Gradient bgGradient;
|
|
||||||
final Brightness brightness;
|
|
||||||
|
|
||||||
ColorTheme.light({
|
ColorTheme.light({
|
||||||
this.black = const Color(0xff000000),
|
this.black = const Color(0xff000000),
|
||||||
this.grey = const Color(0xff7a7a7a),
|
this.grey = const Color(0xff7a7a7a),
|
||||||
@@ -543,19 +550,16 @@ class ColorTheme {
|
|||||||
stops: [0, 1],
|
stops: [0, 1],
|
||||||
),
|
),
|
||||||
this.borderTop = const Effect(
|
this.borderTop = const Effect(
|
||||||
sigmaX: 0,
|
sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08),
|
||||||
sigmaY: -1,
|
|
||||||
color: Color(0xff000000),
|
|
||||||
blur: 0.0,
|
|
||||||
alpha: 0.08),
|
|
||||||
this.borderBottom = const Effect(
|
this.borderBottom = const Effect(
|
||||||
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0, alpha: 0.08),
|
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08),
|
||||||
this.shadowIconButton = const Effect(
|
this.shadowIconButton = const Effect(
|
||||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
|
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4),
|
||||||
this.modalShadow = const Effect(
|
this.modalShadow = const Effect(
|
||||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
|
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8),
|
||||||
}) : brightness = Brightness.light;
|
}) : brightness = Brightness.light;
|
||||||
|
|
||||||
|
/// Initialise with dark theme
|
||||||
ColorTheme.dark({
|
ColorTheme.dark({
|
||||||
this.black = const Color(0xffffffff),
|
this.black = const Color(0xffffffff),
|
||||||
this.grey = const Color(0xff7a7a7a),
|
this.grey = const Color(0xff7a7a7a),
|
||||||
@@ -572,28 +576,28 @@ class ColorTheme {
|
|||||||
sigmaX: 0,
|
sigmaX: 0,
|
||||||
sigmaY: -1,
|
sigmaY: -1,
|
||||||
color: Color(0xff141924),
|
color: Color(0xff141924),
|
||||||
blur: 0.0,
|
blur: 0,
|
||||||
),
|
),
|
||||||
this.borderBottom = const Effect(
|
this.borderBottom = const Effect(
|
||||||
sigmaX: 0,
|
sigmaX: 0,
|
||||||
sigmaY: 1,
|
sigmaY: 1,
|
||||||
color: Color(0xff141924),
|
color: Color(0xff141924),
|
||||||
blur: 0.0,
|
blur: 0,
|
||||||
alpha: 1.0,
|
alpha: 1,
|
||||||
),
|
),
|
||||||
this.shadowIconButton = const Effect(
|
this.shadowIconButton = const Effect(
|
||||||
sigmaX: 0,
|
sigmaX: 0,
|
||||||
sigmaY: 2,
|
sigmaY: 2,
|
||||||
color: Color(0xff000000),
|
color: Color(0xff000000),
|
||||||
alpha: 0.5,
|
alpha: 0.5,
|
||||||
blur: 4.0,
|
blur: 4,
|
||||||
),
|
),
|
||||||
this.modalShadow = const Effect(
|
this.modalShadow = const Effect(
|
||||||
sigmaX: 0,
|
sigmaX: 0,
|
||||||
sigmaY: 0,
|
sigmaY: 0,
|
||||||
color: Color(0xff000000),
|
color: Color(0xff000000),
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
blur: 8.0,
|
blur: 8,
|
||||||
),
|
),
|
||||||
this.highlight = const Color(0xff302d22),
|
this.highlight = const Color(0xff302d22),
|
||||||
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
|
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
|
||||||
@@ -609,6 +613,67 @@ class ColorTheme {
|
|||||||
),
|
),
|
||||||
}) : brightness = Brightness.dark;
|
}) : brightness = Brightness.dark;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color black;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color grey;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color greyGainsboro;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color greyWhisper;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color whiteSmoke;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color whiteSnow;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color white;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color blueAlice;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color accentBlue;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color accentRed;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color accentGreen;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Effect borderTop;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Effect borderBottom;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Effect shadowIconButton;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Effect modalShadow;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color highlight;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color overlay;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color overlayDark;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Gradient bgGradient;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Brightness brightness;
|
||||||
|
|
||||||
|
/// Copy with theme
|
||||||
ColorTheme copyWith({
|
ColorTheme copyWith({
|
||||||
Brightness brightness = Brightness.light,
|
Brightness brightness = Brightness.light,
|
||||||
Color? black,
|
Color? black,
|
||||||
@@ -630,8 +695,8 @@ class ColorTheme {
|
|||||||
Color? overlay,
|
Color? overlay,
|
||||||
Color? overlayDark,
|
Color? overlayDark,
|
||||||
Gradient? bgGradient,
|
Gradient? bgGradient,
|
||||||
}) {
|
}) =>
|
||||||
return brightness == Brightness.light
|
brightness == Brightness.light
|
||||||
? ColorTheme.light(
|
? ColorTheme.light(
|
||||||
black: black ?? this.black,
|
black: black ?? this.black,
|
||||||
grey: grey ?? this.grey,
|
grey: grey ?? this.grey,
|
||||||
@@ -674,8 +739,8 @@ class ColorTheme {
|
|||||||
overlayDark: overlayDark ?? this.overlayDark,
|
overlayDark: overlayDark ?? this.overlayDark,
|
||||||
bgGradient: bgGradient ?? this.bgGradient,
|
bgGradient: bgGradient ?? this.bgGradient,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Merge color theme
|
||||||
ColorTheme merge(ColorTheme? other) {
|
ColorTheme merge(ColorTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -704,13 +769,14 @@ class ColorTheme {
|
|||||||
|
|
||||||
/// Channel theme data
|
/// Channel theme data
|
||||||
class ChannelTheme {
|
class ChannelTheme {
|
||||||
/// Theme of the [ChannelHeader] widget
|
/// Constructor for creating [ChannelTheme]
|
||||||
final ChannelHeaderTheme channelHeaderTheme;
|
|
||||||
|
|
||||||
ChannelTheme({
|
ChannelTheme({
|
||||||
required this.channelHeaderTheme,
|
required this.channelHeaderTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Theme of the [ChannelHeader] widget
|
||||||
|
final ChannelHeaderTheme channelHeaderTheme;
|
||||||
|
|
||||||
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
|
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
|
||||||
ChannelTheme copyWith({
|
ChannelTheme copyWith({
|
||||||
ChannelHeaderTheme? channelHeaderTheme,
|
ChannelHeaderTheme? channelHeaderTheme,
|
||||||
@@ -719,6 +785,7 @@ class ChannelTheme {
|
|||||||
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
|
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge with theme
|
||||||
ChannelTheme merge(ChannelTheme? other) {
|
ChannelTheme merge(ChannelTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -727,28 +794,30 @@ class ChannelTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Theme for avatar
|
||||||
class AvatarTheme {
|
class AvatarTheme {
|
||||||
final BoxConstraints? _constraints;
|
/// Constructor for creating [AvatarTheme]
|
||||||
final BorderRadius? _borderRadius;
|
|
||||||
|
|
||||||
BoxConstraints get constraints {
|
|
||||||
return _constraints ??
|
|
||||||
BoxConstraints.tightFor(
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BorderRadius get borderRadius {
|
|
||||||
return _borderRadius ?? BorderRadius.circular(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvatarTheme({
|
AvatarTheme({
|
||||||
BoxConstraints? constraints,
|
BoxConstraints? constraints,
|
||||||
BorderRadius? borderRadius,
|
BorderRadius? borderRadius,
|
||||||
}) : _constraints = constraints,
|
}) : _constraints = constraints,
|
||||||
_borderRadius = borderRadius;
|
_borderRadius = borderRadius;
|
||||||
|
|
||||||
|
final BoxConstraints? _constraints;
|
||||||
|
final BorderRadius? _borderRadius;
|
||||||
|
|
||||||
|
/// Get constraints for avatar
|
||||||
|
BoxConstraints get constraints =>
|
||||||
|
_constraints ??
|
||||||
|
const BoxConstraints.tightFor(
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Get border radius
|
||||||
|
BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20);
|
||||||
|
|
||||||
|
/// Copy with another theme
|
||||||
AvatarTheme copyWith({
|
AvatarTheme copyWith({
|
||||||
BoxConstraints? constraints,
|
BoxConstraints? constraints,
|
||||||
BorderRadius? borderRadius,
|
BorderRadius? borderRadius,
|
||||||
@@ -758,6 +827,7 @@ class AvatarTheme {
|
|||||||
borderRadius: borderRadius ?? _borderRadius,
|
borderRadius: borderRadius ?? _borderRadius,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge with another AvatarTheme
|
||||||
AvatarTheme merge(AvatarTheme? other) {
|
AvatarTheme merge(AvatarTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -767,19 +837,9 @@ class AvatarTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for getting message theme
|
||||||
class MessageTheme {
|
class MessageTheme {
|
||||||
final TextStyle? messageText;
|
/// Constructor into [MessageTheme]
|
||||||
final TextStyle? messageAuthor;
|
|
||||||
final TextStyle? messageLinks;
|
|
||||||
final TextStyle? createdAt;
|
|
||||||
final TextStyle? replies;
|
|
||||||
final Color? messageBackgroundColor;
|
|
||||||
final Color? messageBorderColor;
|
|
||||||
final Color? reactionsBackgroundColor;
|
|
||||||
final Color? reactionsBorderColor;
|
|
||||||
final Color? reactionsMaskColor;
|
|
||||||
final AvatarTheme? avatarTheme;
|
|
||||||
|
|
||||||
const MessageTheme({
|
const MessageTheme({
|
||||||
this.replies,
|
this.replies,
|
||||||
this.messageText,
|
this.messageText,
|
||||||
@@ -794,6 +854,40 @@ class MessageTheme {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Text style for message text
|
||||||
|
final TextStyle? messageText;
|
||||||
|
|
||||||
|
/// Text style for message author
|
||||||
|
final TextStyle? messageAuthor;
|
||||||
|
|
||||||
|
/// Text style for message links
|
||||||
|
final TextStyle? messageLinks;
|
||||||
|
|
||||||
|
/// Text style for created at text
|
||||||
|
final TextStyle? createdAt;
|
||||||
|
|
||||||
|
/// Text style for replies
|
||||||
|
final TextStyle? replies;
|
||||||
|
|
||||||
|
/// Color for messageBackgroundColor
|
||||||
|
final Color? messageBackgroundColor;
|
||||||
|
|
||||||
|
/// Color for message border color
|
||||||
|
final Color? messageBorderColor;
|
||||||
|
|
||||||
|
/// Color for reactions
|
||||||
|
final Color? reactionsBackgroundColor;
|
||||||
|
|
||||||
|
/// Colors reaction border
|
||||||
|
final Color? reactionsBorderColor;
|
||||||
|
|
||||||
|
/// Color for reaction mask
|
||||||
|
final Color? reactionsMaskColor;
|
||||||
|
|
||||||
|
/// Theme of the avatar
|
||||||
|
final AvatarTheme? avatarTheme;
|
||||||
|
|
||||||
|
/// Copy with a theme
|
||||||
MessageTheme copyWith({
|
MessageTheme copyWith({
|
||||||
TextStyle? messageText,
|
TextStyle? messageText,
|
||||||
TextStyle? messageAuthor,
|
TextStyle? messageAuthor,
|
||||||
@@ -823,6 +917,7 @@ class MessageTheme {
|
|||||||
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
|
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge with a theme
|
||||||
MessageTheme merge(MessageTheme? other) {
|
MessageTheme merge(MessageTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -843,14 +938,9 @@ class MessageTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Theme for channel preview
|
||||||
class ChannelPreviewTheme {
|
class ChannelPreviewTheme {
|
||||||
final TextStyle? title;
|
/// Constructor for creating [ChannelPreviewTheme]
|
||||||
final TextStyle? subtitle;
|
|
||||||
final TextStyle? lastMessageAt;
|
|
||||||
final AvatarTheme? avatarTheme;
|
|
||||||
final Color? unreadCounterColor;
|
|
||||||
final double? indicatorIconSize;
|
|
||||||
|
|
||||||
const ChannelPreviewTheme({
|
const ChannelPreviewTheme({
|
||||||
this.title,
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
@@ -860,6 +950,25 @@ class ChannelPreviewTheme {
|
|||||||
this.indicatorIconSize,
|
this.indicatorIconSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Theme for title
|
||||||
|
final TextStyle? title;
|
||||||
|
|
||||||
|
/// Theme for subtitle
|
||||||
|
final TextStyle? subtitle;
|
||||||
|
|
||||||
|
/// Theme of last message at
|
||||||
|
final TextStyle? lastMessageAt;
|
||||||
|
|
||||||
|
/// Avatar theme
|
||||||
|
final AvatarTheme? avatarTheme;
|
||||||
|
|
||||||
|
/// Unread counter color
|
||||||
|
final Color? unreadCounterColor;
|
||||||
|
|
||||||
|
/// Indicator icon size
|
||||||
|
final double? indicatorIconSize;
|
||||||
|
|
||||||
|
/// Copy with theme
|
||||||
ChannelPreviewTheme copyWith({
|
ChannelPreviewTheme copyWith({
|
||||||
TextStyle? title,
|
TextStyle? title,
|
||||||
TextStyle? subtitle,
|
TextStyle? subtitle,
|
||||||
@@ -877,6 +986,7 @@ class ChannelPreviewTheme {
|
|||||||
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
|
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge with theme
|
||||||
ChannelPreviewTheme merge(ChannelPreviewTheme? other) {
|
ChannelPreviewTheme merge(ChannelPreviewTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -890,12 +1000,9 @@ class ChannelPreviewTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Theme for [ChannelHeader]
|
||||||
class ChannelHeaderTheme {
|
class ChannelHeaderTheme {
|
||||||
final TextStyle? title;
|
/// Constructor for creating a [ChannelHeaderTheme]
|
||||||
final TextStyle? subtitle;
|
|
||||||
final AvatarTheme? avatarTheme;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
const ChannelHeaderTheme({
|
const ChannelHeaderTheme({
|
||||||
this.title,
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
@@ -903,6 +1010,19 @@ class ChannelHeaderTheme {
|
|||||||
this.color,
|
this.color,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Theme for title
|
||||||
|
final TextStyle? title;
|
||||||
|
|
||||||
|
/// Theme for subtitle
|
||||||
|
final TextStyle? subtitle;
|
||||||
|
|
||||||
|
/// Theme for avatar
|
||||||
|
final AvatarTheme? avatarTheme;
|
||||||
|
|
||||||
|
/// Color for [ChannelHeaderTheme]
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// Copy with theme
|
||||||
ChannelHeaderTheme copyWith({
|
ChannelHeaderTheme copyWith({
|
||||||
TextStyle? title,
|
TextStyle? title,
|
||||||
TextStyle? subtitle,
|
TextStyle? subtitle,
|
||||||
@@ -916,6 +1036,7 @@ class ChannelHeaderTheme {
|
|||||||
color: color ?? this.color,
|
color: color ?? this.color,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Merge with other [ChannelHeaderTheme]
|
||||||
ChannelHeaderTheme merge(ChannelHeaderTheme? other) {
|
ChannelHeaderTheme merge(ChannelHeaderTheme? other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
@@ -929,6 +1050,13 @@ class ChannelHeaderTheme {
|
|||||||
|
|
||||||
/// Theme dedicated to the [ChannelListHeader]
|
/// Theme dedicated to the [ChannelListHeader]
|
||||||
class ChannelListHeaderTheme {
|
class ChannelListHeaderTheme {
|
||||||
|
/// Returns a new [ChannelListHeaderTheme]
|
||||||
|
const ChannelListHeaderTheme({
|
||||||
|
this.title,
|
||||||
|
this.avatarTheme,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
|
||||||
/// Style of the title text
|
/// Style of the title text
|
||||||
final TextStyle? title;
|
final TextStyle? title;
|
||||||
|
|
||||||
@@ -938,13 +1066,6 @@ class ChannelListHeaderTheme {
|
|||||||
/// Background color of the appbar
|
/// Background color of the appbar
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
||||||
/// Returns a new [ChannelListHeaderTheme]
|
|
||||||
const ChannelListHeaderTheme({
|
|
||||||
this.title,
|
|
||||||
this.avatarTheme,
|
|
||||||
this.color,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Returns a new [ChannelListHeaderTheme] replacing some of its properties
|
/// Returns a new [ChannelListHeaderTheme] replacing some of its properties
|
||||||
ChannelListHeaderTheme copyWith({
|
ChannelListHeaderTheme copyWith({
|
||||||
TextStyle? title,
|
TextStyle? title,
|
||||||
@@ -970,6 +1091,22 @@ class ChannelListHeaderTheme {
|
|||||||
|
|
||||||
/// Defines the theme dedicated to the [MessageInput] widget
|
/// Defines the theme dedicated to the [MessageInput] widget
|
||||||
class MessageInputTheme {
|
class MessageInputTheme {
|
||||||
|
/// Returns a new [MessageInputTheme]
|
||||||
|
const MessageInputTheme({
|
||||||
|
this.sendAnimationDuration,
|
||||||
|
this.actionButtonColor,
|
||||||
|
this.sendButtonColor,
|
||||||
|
this.actionButtonIdleColor,
|
||||||
|
this.sendButtonIdleColor,
|
||||||
|
this.inputBackground,
|
||||||
|
this.inputTextStyle,
|
||||||
|
this.inputDecoration,
|
||||||
|
this.activeBorderGradient,
|
||||||
|
this.idleBorderGradient,
|
||||||
|
this.borderRadius,
|
||||||
|
this.expandButtonColor,
|
||||||
|
});
|
||||||
|
|
||||||
/// Duration of the [MessageInput] send button animation
|
/// Duration of the [MessageInput] send button animation
|
||||||
final Duration? sendAnimationDuration;
|
final Duration? sendAnimationDuration;
|
||||||
|
|
||||||
@@ -1006,22 +1143,6 @@ class MessageInputTheme {
|
|||||||
/// Border radius of [MessageInput]
|
/// Border radius of [MessageInput]
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
/// Returns a new [MessageInputTheme]
|
|
||||||
const MessageInputTheme({
|
|
||||||
this.sendAnimationDuration,
|
|
||||||
this.actionButtonColor,
|
|
||||||
this.sendButtonColor,
|
|
||||||
this.actionButtonIdleColor,
|
|
||||||
this.sendButtonIdleColor,
|
|
||||||
this.inputBackground,
|
|
||||||
this.inputTextStyle,
|
|
||||||
this.inputDecoration,
|
|
||||||
this.activeBorderGradient,
|
|
||||||
this.idleBorderGradient,
|
|
||||||
this.borderRadius,
|
|
||||||
this.expandButtonColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Returns a new [MessageInputTheme] replacing some of its properties
|
/// Returns a new [MessageInputTheme] replacing some of its properties
|
||||||
MessageInputTheme copyWith({
|
MessageInputTheme copyWith({
|
||||||
Duration? sendAnimationDuration,
|
Duration? sendAnimationDuration,
|
||||||
@@ -1075,13 +1196,9 @@ class MessageInputTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Effect store
|
||||||
class Effect {
|
class Effect {
|
||||||
final double? sigmaX;
|
/// Constructor for creating [Effect]
|
||||||
final double? sigmaY;
|
|
||||||
final Color? color;
|
|
||||||
final double? alpha;
|
|
||||||
final double? blur;
|
|
||||||
|
|
||||||
const Effect({
|
const Effect({
|
||||||
this.sigmaX,
|
this.sigmaX,
|
||||||
this.sigmaY,
|
this.sigmaY,
|
||||||
@@ -1090,6 +1207,22 @@ class Effect {
|
|||||||
this.blur,
|
this.blur,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
final double? sigmaX;
|
||||||
|
|
||||||
|
///
|
||||||
|
final double? sigmaY;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
///
|
||||||
|
final double? alpha;
|
||||||
|
|
||||||
|
///
|
||||||
|
final double? blur;
|
||||||
|
|
||||||
|
/// Copy with new effect
|
||||||
Effect copyWith({
|
Effect copyWith({
|
||||||
double? sigmaX,
|
double? sigmaX,
|
||||||
double? sigmaY,
|
double? sigmaY,
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Neumorphic button
|
||||||
class StreamNeumorphicButton extends StatelessWidget {
|
class StreamNeumorphicButton extends StatelessWidget {
|
||||||
final Widget child;
|
/// Constructor for creating [StreamNeumorphicButton]
|
||||||
final Color backgroundColor;
|
|
||||||
|
|
||||||
const StreamNeumorphicButton({
|
const StreamNeumorphicButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.backgroundColor = Colors.white,
|
this.backgroundColor = Colors.white,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Child contained in the button
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Background color of button
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Container(
|
||||||
return Container(
|
margin: const EdgeInsets.all(8),
|
||||||
margin: EdgeInsets.all(8.0),
|
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -22,19 +26,15 @@ class StreamNeumorphicButton extends StatelessWidget {
|
|||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
offset: Offset(0, 1.0),
|
offset: const Offset(0, 1),
|
||||||
blurRadius: 0.5,
|
blurRadius: 0.5,
|
||||||
spreadRadius: 0,
|
|
||||||
),
|
),
|
||||||
BoxShadow(
|
const BoxShadow(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
offset: Offset.zero,
|
|
||||||
blurRadius: 0.5,
|
blurRadius: 0.5,
|
||||||
spreadRadius: 0,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,45 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'stream_chat_theme.dart';
|
/// Widget to make a swipeable tile
|
||||||
|
|
||||||
///
|
|
||||||
class Swipeable extends StatefulWidget {
|
class Swipeable extends StatefulWidget {
|
||||||
final Widget child;
|
/// Constructor for creating a [Swipeable] widget
|
||||||
final Widget backgroundIcon;
|
|
||||||
final VoidCallback? onSwipeStart;
|
|
||||||
final VoidCallback? onSwipeCancel;
|
|
||||||
final VoidCallback? onSwipeEnd;
|
|
||||||
final double threshold;
|
|
||||||
|
|
||||||
///
|
|
||||||
const Swipeable({
|
const Swipeable({
|
||||||
|
Key? key,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.backgroundIcon,
|
required this.backgroundIcon,
|
||||||
this.onSwipeStart,
|
this.onSwipeStart,
|
||||||
this.onSwipeCancel,
|
this.onSwipeCancel,
|
||||||
this.onSwipeEnd,
|
this.onSwipeEnd,
|
||||||
this.threshold = 82.0,
|
this.threshold = 82.0,
|
||||||
});
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Child to make swipeable
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Background icon after swipe
|
||||||
|
final Widget backgroundIcon;
|
||||||
|
|
||||||
|
/// Callback when swipe starts
|
||||||
|
final VoidCallback? onSwipeStart;
|
||||||
|
|
||||||
|
/// Callback when swipe is cancelled
|
||||||
|
final VoidCallback? onSwipeCancel;
|
||||||
|
|
||||||
|
/// Callback when swipe ends
|
||||||
|
final VoidCallback? onSwipeEnd;
|
||||||
|
|
||||||
|
/// Threshold for swipe
|
||||||
|
final double threshold;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _SwipeableState();
|
State<StatefulWidget> createState() => _SwipeableState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
||||||
double _dragExtent = 0.0;
|
double _dragExtent = 0;
|
||||||
late AnimationController _moveController;
|
late AnimationController _moveController;
|
||||||
late AnimationController _iconMoveController;
|
late AnimationController _iconMoveController;
|
||||||
late Animation<Offset> _moveAnimation;
|
late Animation<Offset> _moveAnimation;
|
||||||
@@ -45,15 +56,15 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
|||||||
AnimationController(duration: _animationDuration, vsync: this);
|
AnimationController(duration: _animationDuration, vsync: this);
|
||||||
_iconMoveController =
|
_iconMoveController =
|
||||||
AnimationController(duration: _animationDuration, vsync: this);
|
AnimationController(duration: _animationDuration, vsync: this);
|
||||||
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
|
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))
|
||||||
.animate(_moveController);
|
.animate(_moveController);
|
||||||
_iconTransitionAnimation =
|
_iconTransitionAnimation =
|
||||||
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0))
|
Tween<Offset>(begin: const Offset(-0.1, 0), end: const Offset(0.4, 0))
|
||||||
.animate(_moveController);
|
.animate(_moveController);
|
||||||
_iconFadeAnimation =
|
_iconFadeAnimation =
|
||||||
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController);
|
Tween<double>(begin: 0.7, end: 1).animate(_iconMoveController);
|
||||||
|
|
||||||
final controllerValue = 0.0;
|
const controllerValue = 0.0;
|
||||||
_moveController.animateTo(controllerValue);
|
_moveController.animateTo(controllerValue);
|
||||||
_iconMoveController.animateTo(controllerValue);
|
_iconMoveController.animateTo(controllerValue);
|
||||||
}
|
}
|
||||||
@@ -77,19 +88,19 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
if (_dragExtent.isNegative) return;
|
if (_dragExtent.isNegative) return;
|
||||||
|
|
||||||
var movePastThresholdPixels = widget.threshold;
|
final movePastThresholdPixels = widget.threshold;
|
||||||
var newPos = _dragExtent.abs() / context.size!.width;
|
var newPos = _dragExtent.abs() / context.size!.width;
|
||||||
|
|
||||||
if (_dragExtent.abs() > movePastThresholdPixels) {
|
if (_dragExtent.abs() > movePastThresholdPixels) {
|
||||||
// how many "thresholds" past the threshold we are. 1 = the threshold 2
|
// how many "thresholds" past the threshold we are. 1 = the threshold 2
|
||||||
// = two thresholds.
|
// = two thresholds.
|
||||||
var n = _dragExtent.abs() / movePastThresholdPixels;
|
final n = _dragExtent.abs() / movePastThresholdPixels;
|
||||||
|
|
||||||
// Take the number of thresholds past the threshold, and reduce this
|
// Take the number of thresholds past the threshold, and reduce this
|
||||||
// number
|
// number
|
||||||
var reducedThreshold = math.pow(n, 0.3);
|
final reducedThreshold = math.pow(n, 0.3);
|
||||||
|
|
||||||
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
|
final adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
|
||||||
newPos = adjustedPixelPos / context.size!.width;
|
newPos = adjustedPixelPos / context.size!.width;
|
||||||
|
|
||||||
if (_dragExtent > 0 && !_pastThreshold) {
|
if (_dragExtent > 0 && !_pastThreshold) {
|
||||||
@@ -111,8 +122,8 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragEnd(DragEndDetails details) {
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
_moveController.animateTo(0.0, duration: _animationDuration);
|
_moveController.animateTo(0, duration: _animationDuration);
|
||||||
_iconMoveController.animateTo(0.0, duration: _animationDuration);
|
_iconMoveController.animateTo(0, duration: _animationDuration);
|
||||||
_dragExtent = 0.0;
|
_dragExtent = 0.0;
|
||||||
if (_pastThreshold && widget.onSwipeEnd != null) {
|
if (_pastThreshold && widget.onSwipeEnd != null) {
|
||||||
widget.onSwipeEnd!();
|
widget.onSwipeEnd!();
|
||||||
@@ -120,8 +131,7 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
return GestureDetector(
|
|
||||||
onHorizontalDragStart: _handleDragStart,
|
onHorizontalDragStart: _handleDragStart,
|
||||||
onHorizontalDragUpdate: _handleDragUpdate,
|
onHorizontalDragUpdate: _handleDragUpdate,
|
||||||
onHorizontalDragEnd: _handleDragEnd,
|
onHorizontalDragEnd: _handleDragEnd,
|
||||||
@@ -160,4 +170,3 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
|
|
||||||
/// It shows a date divider depending on the date difference
|
/// It shows a date divider depending on the date difference
|
||||||
class SystemMessage extends StatelessWidget {
|
class SystemMessage extends StatelessWidget {
|
||||||
/// This message
|
/// Constructor for creating a [SystemMessage]
|
||||||
final Message message;
|
|
||||||
|
|
||||||
/// The function called when tapping on the message when the message is not failed
|
|
||||||
final void Function(Message)? onMessageTap;
|
|
||||||
|
|
||||||
const SystemMessage({
|
const SystemMessage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.onMessageTap,
|
this.onMessageTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// This message
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
/// The function called when tapping on the message when the message is not failed
|
||||||
|
final void Function(Message)? onMessageTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = StreamChatTheme.of(context);
|
final theme = StreamChatTheme.of(context);
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'back_button.dart';
|
|
||||||
import 'channel_name.dart';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
@@ -46,16 +43,34 @@ import 'channel_name.dart';
|
|||||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||||
/// However you can also use it as a normal widget.
|
/// However you can also use it as a normal widget.
|
||||||
///
|
///
|
||||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
|
/// Make sure to have a [StreamChannel] ancestor in order to provide the
|
||||||
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
|
/// information about the channel.
|
||||||
|
/// Every part of the widget uses a [StreamBuilder] to render the channel
|
||||||
|
/// information as soon as it updates.
|
||||||
///
|
///
|
||||||
/// By default the widget shows a backButton that calls [Navigator.pop].
|
/// By default the widget shows a backButton that calls [Navigator.pop].
|
||||||
/// You can disable this button using the [showBackButton] property of just override the behaviour
|
/// You can disable this button using the [showBackButton] property of just
|
||||||
|
/// override the behaviour
|
||||||
/// with [onBackPressed].
|
/// with [onBackPressed].
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
/// The widget components render the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
/// Instantiate a new ThreadHeader
|
||||||
|
const ThreadHeader({
|
||||||
|
Key? key,
|
||||||
|
required this.parent,
|
||||||
|
this.showBackButton = true,
|
||||||
|
this.onBackPressed,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.actions,
|
||||||
|
this.onTitleTap,
|
||||||
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// True if this header shows the leading back button
|
/// True if this header shows the leading back button
|
||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
|
|
||||||
@@ -81,23 +96,8 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// AppBar actions
|
/// AppBar actions
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
/// Instantiate a new ThreadHeader
|
|
||||||
ThreadHeader({
|
|
||||||
Key? key,
|
|
||||||
required this.parent,
|
|
||||||
this.showBackButton = true,
|
|
||||||
this.onBackPressed,
|
|
||||||
this.title,
|
|
||||||
this.subtitle,
|
|
||||||
this.leading,
|
|
||||||
this.actions,
|
|
||||||
this.onTitleTap,
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => AppBar(
|
||||||
return AppBar(
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@@ -108,17 +108,16 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
showUnreads: true,
|
showUnreads: true,
|
||||||
)
|
)
|
||||||
: SizedBox()),
|
: const SizedBox()),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
title: InkWell(
|
title: InkWell(
|
||||||
onTap: onTitleTap,
|
onTap: onTitleTap,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: preferredSize.height,
|
height: preferredSize.height,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
title ??
|
title ??
|
||||||
@@ -129,11 +128,10 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
.channelHeaderTheme
|
.channelHeaderTheme
|
||||||
.title,
|
.title,
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
subtitle ??
|
subtitle ??
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -158,7 +156,6 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Size preferredSize;
|
final Size preferredSize;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
/// The padding of this widget
|
/// The padding of this widget
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
/// Alignment of the typing indicator
|
||||||
final Alignment alignment;
|
final Alignment alignment;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,14 +36,13 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
return StreamBuilder<List<User>>(
|
return StreamBuilder<List<User>>(
|
||||||
initialData: channelState.typingEvents,
|
initialData: channelState.typingEvents,
|
||||||
stream: channelState.typingEventsStream,
|
stream: channelState.typingEventsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => AnimatedSwitcher(
|
||||||
return AnimatedSwitcher(
|
duration: const Duration(milliseconds: 300),
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
child: snapshot.data?.isNotEmpty == true
|
child: snapshot.data?.isNotEmpty == true
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Align(
|
child: Align(
|
||||||
key: Key('typings'),
|
key: const Key('typings'),
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -53,6 +53,7 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
height: 4,
|
height: 4,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
|
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: style,
|
style: style,
|
||||||
@@ -62,14 +63,13 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(
|
||||||
key: Key('alternative'),
|
key: const Key('alternative'),
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Container(
|
child: Container(
|
||||||
child: alternativeWidget ?? Offstage(),
|
child: alternativeWidget ?? const Offstage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Widget for showing an unread indicator
|
||||||
class UnreadIndicator extends StatelessWidget {
|
class UnreadIndicator extends StatelessWidget {
|
||||||
|
/// Constructor for creating an [UnreadIndicator]
|
||||||
const UnreadIndicator({
|
const UnreadIndicator({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.cid,
|
this.cid,
|
||||||
@@ -24,7 +26,7 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
: client.state.totalUnreadCount,
|
: client.state.totalUnreadCount,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || snapshot.data == 0) {
|
if (!snapshot.hasData || snapshot.data == 0) {
|
||||||
return SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
return Material(
|
return Material(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -33,15 +35,15 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
.unreadCounterColor,
|
.unreadCounterColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 5.0,
|
left: 5,
|
||||||
right: 5.0,
|
right: 5,
|
||||||
top: 2,
|
top: 2,
|
||||||
bottom: 1,
|
bottom: 1,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${snapshot.data! > 99 ? '99+' : snapshot.data}',
|
'${snapshot.data! > 99 ? '99+' : snapshot.data}',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'stream_chat_theme.dart';
|
/// Widget for showing upload progress
|
||||||
|
|
||||||
class UploadProgressIndicator extends StatelessWidget {
|
class UploadProgressIndicator extends StatelessWidget {
|
||||||
final int uploaded;
|
/// Constructor for creating an [UploadProgressIndicator]
|
||||||
final int total;
|
const UploadProgressIndicator({
|
||||||
final Color progressIndicatorColor;
|
|
||||||
final EdgeInsetsGeometry padding;
|
|
||||||
final bool showBackground;
|
|
||||||
final TextStyle? textStyle;
|
|
||||||
|
|
||||||
UploadProgressIndicator({
|
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.uploaded,
|
required this.uploaded,
|
||||||
required this.total,
|
required this.total,
|
||||||
@@ -25,6 +19,24 @@ class UploadProgressIndicator extends StatelessWidget {
|
|||||||
this.textStyle,
|
this.textStyle,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Bytes uploaded
|
||||||
|
final int uploaded;
|
||||||
|
|
||||||
|
/// Total bytes
|
||||||
|
final int total;
|
||||||
|
|
||||||
|
/// Color of progress indicator
|
||||||
|
final Color progressIndicatorColor;
|
||||||
|
|
||||||
|
/// Padding for widget
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
/// Flag for showing background
|
||||||
|
final bool showBackground;
|
||||||
|
|
||||||
|
/// [TextStyle] to be applied to text
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = StreamChatTheme.of(context);
|
final theme = StreamChatTheme.of(context);
|
||||||
@@ -42,7 +54,7 @@ class UploadProgressIndicator extends StatelessWidget {
|
|||||||
valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
|
valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'${_percentage.toInt()}%',
|
'${_percentage.toInt()}%',
|
||||||
style: textStyle ??
|
style: textStyle ??
|
||||||
|
|||||||
@@ -3,23 +3,30 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Widget to display URL attachment
|
||||||
class UrlAttachment extends StatelessWidget {
|
class UrlAttachment extends StatelessWidget {
|
||||||
final Attachment urlAttachment;
|
/// Constructor for creating a [UrlAttachment]
|
||||||
final String hostDisplayName;
|
const UrlAttachment({
|
||||||
final EdgeInsets textPadding;
|
Key? key,
|
||||||
|
|
||||||
UrlAttachment({
|
|
||||||
required this.urlAttachment,
|
required this.urlAttachment,
|
||||||
required this.hostDisplayName,
|
required this.hostDisplayName,
|
||||||
this.textPadding = const EdgeInsets.symmetric(
|
this.textPadding = const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16,
|
||||||
vertical: 8.0,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
});
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Attachment to be displayed
|
||||||
|
final Attachment urlAttachment;
|
||||||
|
|
||||||
|
/// Host name
|
||||||
|
final String hostDisplayName;
|
||||||
|
|
||||||
|
/// Padding for text
|
||||||
|
final EdgeInsets textPadding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchURL(
|
launchURL(
|
||||||
context,
|
context,
|
||||||
@@ -32,9 +39,9 @@ class UrlAttachment extends StatelessWidget {
|
|||||||
if (urlAttachment.imageUrl != null)
|
if (urlAttachment.imageUrl != null)
|
||||||
Container(
|
Container(
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -44,20 +51,21 @@ class UrlAttachment extends StatelessWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0.0,
|
left: 0,
|
||||||
bottom: -1,
|
bottom: -1,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
color: StreamChatTheme.of(context).colorTheme.blueAlice,
|
color:
|
||||||
|
StreamChatTheme.of(context).colorTheme.blueAlice,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 8.0,
|
top: 8,
|
||||||
left: 8.0,
|
left: 8,
|
||||||
right: 8.0,
|
right: 8,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
hostDisplayName,
|
hostDisplayName,
|
||||||
@@ -106,4 +114,3 @@ class UrlAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Widget that displays a user avatar
|
||||||
|
|
||||||
class UserAvatar extends StatelessWidget {
|
class UserAvatar extends StatelessWidget {
|
||||||
|
/// Constructor to create a [UserAvatar]
|
||||||
const UserAvatar({
|
const UserAvatar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.user,
|
required this.user,
|
||||||
@@ -20,16 +21,37 @@ class UserAvatar extends StatelessWidget {
|
|||||||
this.selectionThickness = 4,
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// User whose avatar is to displayed
|
||||||
final User user;
|
final User user;
|
||||||
|
|
||||||
|
/// Alignment of the online indicator
|
||||||
final Alignment onlineIndicatorAlignment;
|
final Alignment onlineIndicatorAlignment;
|
||||||
|
|
||||||
|
/// Size of the avatar
|
||||||
final BoxConstraints? constraints;
|
final BoxConstraints? constraints;
|
||||||
|
|
||||||
|
/// [BorderRadius] of the image
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
|
/// Size of the online indicator
|
||||||
final BoxConstraints? onlineIndicatorConstraints;
|
final BoxConstraints? onlineIndicatorConstraints;
|
||||||
|
|
||||||
|
/// Callback when avatar is tapped
|
||||||
final void Function(User)? onTap;
|
final void Function(User)? onTap;
|
||||||
|
|
||||||
|
/// Callback when avatar is long pressed
|
||||||
final void Function(User)? onLongPress;
|
final void Function(User)? onLongPress;
|
||||||
|
|
||||||
|
/// Flag for showing online status
|
||||||
final bool showOnlineStatus;
|
final bool showOnlineStatus;
|
||||||
|
|
||||||
|
/// Flag for if avatar is selected
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
|
||||||
|
/// Color of selection
|
||||||
final Color? selectionColor;
|
final Color? selectionColor;
|
||||||
|
|
||||||
|
/// Selection thickness around the avatar
|
||||||
final double selectionThickness;
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,7 +64,6 @@ class UserAvatar extends StatelessWidget {
|
|||||||
Widget avatar = FittedBox(
|
Widget avatar = FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
borderRadius: borderRadius ??
|
borderRadius: borderRadius ??
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -54,10 +75,10 @@ class UserAvatar extends StatelessWidget {
|
|||||||
child: hasImage
|
child: hasImage
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
imageUrl: user.extraData['image'] as String,
|
imageUrl: user.extraData['image'] as String,
|
||||||
errorWidget: (_, __, ___) {
|
errorWidget: (_, __, ___) =>
|
||||||
return streamChatTheme.defaultUserImage(context, user);
|
streamChatTheme.defaultUserImage(context, user),
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: streamChatTheme.defaultUserImage(context, user),
|
: streamChatTheme.defaultUserImage(context, user),
|
||||||
@@ -97,14 +118,14 @@ class UserAvatar extends StatelessWidget {
|
|||||||
type: MaterialType.circle,
|
type: MaterialType.circle,
|
||||||
color: streamChatTheme.colorTheme.white,
|
color: streamChatTheme.colorTheme.white,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(2.0),
|
margin: const EdgeInsets.all(2),
|
||||||
constraints: onlineIndicatorConstraints ??
|
constraints: onlineIndicatorConstraints ??
|
||||||
BoxConstraints.tightFor(
|
const BoxConstraints.tightFor(
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
shape: CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
color: streamChatTheme.colorTheme.accentGreen,
|
color: streamChatTheme.colorTheme.accentGreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import 'package:stream_chat_flutter/src/user_list_view.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'stream_chat_theme.dart';
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// It shows the current [User] preview.
|
/// It shows the current [User] preview.
|
||||||
///
|
///
|
||||||
/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates.
|
/// The widget uses a [StreamBuilder] to render the user information
|
||||||
|
/// image as soon as it updates.
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's the default user preview used by [UserListView].
|
/// Usually you don't use this widget as it's the default user preview used
|
||||||
|
/// by [UserListView].
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget renders the ui based on the first ancestor of type
|
||||||
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class UserItem extends StatelessWidget {
|
class UserItem extends StatelessWidget {
|
||||||
/// Instantiate a new UserItem
|
/// Instantiate a new UserItem
|
||||||
@@ -47,8 +48,7 @@ class UserItem extends StatelessWidget {
|
|||||||
final bool showLastOnline;
|
final bool showLastOnline;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => ListTile(
|
||||||
return ListTile(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap != null) {
|
if (onTap != null) {
|
||||||
onTap!(user);
|
onTap!(user);
|
||||||
@@ -61,13 +61,12 @@ class UserItem extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
leading: UserAvatar(
|
leading: UserAvatar(
|
||||||
user: user,
|
user: user,
|
||||||
showOnlineStatus: true,
|
|
||||||
onTap: (user) {
|
onTap: (user) {
|
||||||
if (onImageTap != null) {
|
if (onImageTap != null) {
|
||||||
onImageTap!(user);
|
onImageTap!(user);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
@@ -83,15 +82,13 @@ class UserItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: showLastOnline ? _buildLastActive(context) : null,
|
subtitle: showLastOnline ? _buildLastActive(context) : null,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLastActive(context) {
|
Widget _buildLastActive(context) => Text(
|
||||||
return Text(
|
|
||||||
user.online == true
|
user.online == true
|
||||||
? 'Online'
|
? 'Online'
|
||||||
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
|
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
|
||||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
|
color:
|
||||||
|
StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'user_item.dart';
|
|
||||||
|
|
||||||
/// Callback called when tapping on a user
|
/// Callback called when tapping on a user
|
||||||
typedef UserTapCallback = void Function(User, Widget?);
|
typedef UserTapCallback = void Function(User, Widget?);
|
||||||
|
|
||||||
@@ -36,10 +34,13 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users.
|
/// Make sure to have a [UsersBloc] ancestor in order to provide the
|
||||||
/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels.
|
/// information about the users.
|
||||||
|
/// The widget uses a [ListView.separated], [GridView.builder] to render the
|
||||||
|
/// list, grid of channels.
|
||||||
///
|
///
|
||||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
/// The widget components render the ui based on the first ancestor of
|
||||||
|
/// type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class UserListView extends StatefulWidget {
|
class UserListView extends StatefulWidget {
|
||||||
/// Instantiate a new UserListView
|
/// Instantiate a new UserListView
|
||||||
@@ -81,8 +82,10 @@ class UserListView extends StatefulWidget {
|
|||||||
final Map<String, dynamic>? options;
|
final Map<String, dynamic>? options;
|
||||||
|
|
||||||
/// The sorting used for the channels matching the filters.
|
/// The sorting used for the channels matching the filters.
|
||||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
/// Sorting is based on field and direction, multiple sorting options can
|
||||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
/// be provided.
|
||||||
|
/// You can sort based on last_updated, last_message_at, updated_at, created_
|
||||||
|
/// at or member_count.
|
||||||
/// Direction can be ascending or descending.
|
/// Direction can be ascending or descending.
|
||||||
final List<SortOption>? sort;
|
final List<SortOption>? sort;
|
||||||
|
|
||||||
@@ -115,7 +118,8 @@ class UserListView extends StatefulWidget {
|
|||||||
/// Set it to false to disable the pull-to-refresh widget
|
/// Set it to false to disable the pull-to-refresh widget
|
||||||
final bool pullToRefresh;
|
final bool pullToRefresh;
|
||||||
|
|
||||||
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
|
/// Sets a blue trailing checkMark in [ListUserItem] for all the
|
||||||
|
/// [selectedUsers]
|
||||||
final Set<User>? selectedUsers;
|
final Set<User>? selectedUsers;
|
||||||
|
|
||||||
/// Set it to true to group users by their first character
|
/// Set it to true to group users by their first character
|
||||||
@@ -151,37 +155,27 @@ class _UserListViewState extends State<UserListView>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var child = UserListCore(
|
final child = UserListCore(
|
||||||
errorBuilder: widget.errorBuilder as Widget Function(Object)? ??
|
errorBuilder: widget.errorBuilder as Widget Function(Object)? ??
|
||||||
(err) {
|
(err) => _buildError(err as Error),
|
||||||
return _buildError(err as Error);
|
emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(),
|
||||||
},
|
|
||||||
emptyBuilder: widget.emptyBuilder ??
|
|
||||||
(context) {
|
|
||||||
return _buildEmpty();
|
|
||||||
},
|
|
||||||
loadingBuilder: widget.loadingBuilder ??
|
loadingBuilder: widget.loadingBuilder ??
|
||||||
(context) {
|
(context) => LayoutBuilder(
|
||||||
return LayoutBuilder(
|
builder: (context, viewportConstraints) =>
|
||||||
builder: (context, viewportConstraints) {
|
SingleChildScrollView(
|
||||||
return SingleChildScrollView(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: viewportConstraints.maxHeight,
|
minHeight: viewportConstraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
listBuilder:
|
||||||
},
|
widget.listBuilder ?? (context, list) => _buildListView(list),
|
||||||
listBuilder: widget.listBuilder ??
|
|
||||||
(context, list) {
|
|
||||||
return _buildListView(list);
|
|
||||||
},
|
|
||||||
pagination: widget.pagination,
|
pagination: widget.pagination,
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
sort: widget.sort,
|
sort: widget.sort,
|
||||||
@@ -204,7 +198,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
||||||
|
|
||||||
Widget _buildError(Error error) {
|
Widget _buildError(Error error) {
|
||||||
print((error).stackTrace);
|
print(error.stackTrace);
|
||||||
|
|
||||||
var message = error.toString();
|
var message = error.toString();
|
||||||
if (error is DioError) {
|
if (error is DioError) {
|
||||||
@@ -220,12 +214,12 @@ class _UserListViewState extends State<UserListView>
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
const TextSpan(
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 2.0,
|
right: 2,
|
||||||
),
|
),
|
||||||
child: Icon(Icons.error_outline),
|
child: Icon(Icons.error_outline),
|
||||||
),
|
),
|
||||||
@@ -237,43 +231,39 @@ class _UserListViewState extends State<UserListView>
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 16.0,
|
top: 16,
|
||||||
),
|
),
|
||||||
child: Text(message),
|
child: Text(message),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => _userListController.loadData!(),
|
onPressed: () => _userListController.loadData!(),
|
||||||
child: Text('Retry'),
|
child: const Text('Retry'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmpty() {
|
Widget _buildEmpty() => LayoutBuilder(
|
||||||
return LayoutBuilder(
|
builder: (context, viewportConstraints) => SingleChildScrollView(
|
||||||
builder: (context, viewportConstraints) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return SingleChildScrollView(
|
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: viewportConstraints.maxHeight,
|
minHeight: viewportConstraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Text('There are no users currently'),
|
child: Text('There are no users currently'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListView(
|
Widget _buildListView(
|
||||||
List<ListItem> items,
|
List<ListItem> items,
|
||||||
) {
|
) {
|
||||||
final child = _isListView
|
final child = _isListView
|
||||||
? ListView.separated(
|
? ListView.separated(
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||||
separatorBuilder: (_, index) {
|
separatorBuilder: (_, index) {
|
||||||
if (widget.separatorBuilder != null) {
|
if (widget.separatorBuilder != null) {
|
||||||
@@ -281,19 +271,17 @@ class _UserListViewState extends State<UserListView>
|
|||||||
}
|
}
|
||||||
return _separatorBuilder(context, index);
|
return _separatorBuilder(context, index);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) =>
|
||||||
return _listItemBuilder(context, index, items);
|
_listItemBuilder(context, index, items),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
: GridView.builder(
|
: GridView.builder(
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: widget.crossAxisCount,
|
crossAxisCount: widget.crossAxisCount,
|
||||||
),
|
),
|
||||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) =>
|
||||||
return _gridItemBuilder(context, index, items);
|
_gridItemBuilder(context, index, items),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return LazyLoadScrollView(
|
return LazyLoadScrollView(
|
||||||
@@ -307,13 +295,11 @@ class _UserListViewState extends State<UserListView>
|
|||||||
if (i < items.length) {
|
if (i < items.length) {
|
||||||
final item = items[i];
|
final item = items[i];
|
||||||
return item.when(
|
return item.when(
|
||||||
headerItem: (header) {
|
headerItem: (header) => Container(
|
||||||
return Container(
|
|
||||||
key: ValueKey<String>('HEADER-$header'),
|
key: ValueKey<String>('HEADER-$header'),
|
||||||
color:
|
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
|
||||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
header,
|
header,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -323,8 +309,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
userItem: (user) {
|
userItem: (user) {
|
||||||
final selected = widget.selectedUsers?.contains(user) ?? false;
|
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||||
return Container(
|
return Container(
|
||||||
@@ -351,7 +336,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
if (i < items.length) {
|
if (i < items.length) {
|
||||||
final item = items[i];
|
final item = items[i];
|
||||||
return item.when(
|
return item.when(
|
||||||
headerItem: (_) => Offstage(),
|
headerItem: (_) => const Offstage(),
|
||||||
userItem: (user) {
|
userItem: (user) {
|
||||||
final selected = widget.selectedUsers?.contains(user) ?? false;
|
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||||
return Container(
|
return Container(
|
||||||
@@ -360,17 +345,17 @@ class _UserListViewState extends State<UserListView>
|
|||||||
? widget.userItemBuilder!(context, user, selected)
|
? widget.userItemBuilder!(context, user, selected)
|
||||||
: Column(
|
: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
user: user,
|
user: user,
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 64,
|
height: 64,
|
||||||
width: 64,
|
width: 64,
|
||||||
),
|
),
|
||||||
onlineIndicatorConstraints: BoxConstraints.tightFor(
|
onlineIndicatorConstraints:
|
||||||
|
const BoxConstraints.tightFor(
|
||||||
height: 12,
|
height: 12,
|
||||||
width: 12,
|
width: 12,
|
||||||
),
|
),
|
||||||
@@ -378,7 +363,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
widget.onUserTap!(user, widget.userWidget),
|
widget.onUserTap!(user, widget.userWidget),
|
||||||
onLongPress: widget.onUserLongPress,
|
onLongPress: widget.onUserLongPress,
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -386,7 +371,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
@@ -402,8 +387,8 @@ class _UserListViewState extends State<UserListView>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
|
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) =>
|
||||||
return StreamBuilder<bool>(
|
StreamBuilder<bool>(
|
||||||
stream: usersProvider.queryUsersLoading,
|
stream: usersProvider.queryUsersLoading,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@@ -413,8 +398,8 @@ class _UserListViewState extends State<UserListView>
|
|||||||
.colorTheme
|
.colorTheme
|
||||||
.accentRed
|
.accentRed
|
||||||
.withOpacity(.2),
|
.withOpacity(.2),
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('Error loading users'),
|
child: Text('Error loading users'),
|
||||||
),
|
),
|
||||||
@@ -423,18 +408,17 @@ class _UserListViewState extends State<UserListView>
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
height: 100,
|
height: 100,
|
||||||
padding: EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: snapshot.data! ? CircularProgressIndicator() : Container(),
|
child: snapshot.data!
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Container(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
Widget _separatorBuilder(context, i) {
|
Widget _separatorBuilder(context, i) => Container(
|
||||||
return Container(
|
|
||||||
height: 1,
|
height: 1,
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// Displays a list of users who reacted
|
||||||
class UserReactionDisplay extends StatelessWidget {
|
class UserReactionDisplay extends StatelessWidget {
|
||||||
|
/// Constructor for creating a [UserReactionDisplay]
|
||||||
const UserReactionDisplay({
|
const UserReactionDisplay({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.reactionToEmoji,
|
required this.reactionToEmoji,
|
||||||
@@ -10,24 +12,27 @@ class UserReactionDisplay extends StatelessWidget {
|
|||||||
this.size = 30,
|
this.size = 30,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Reaction map
|
||||||
final Map<String, String> reactionToEmoji;
|
final Map<String, String> reactionToEmoji;
|
||||||
|
|
||||||
|
/// Message which is reacted to
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// Size of Icon
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Container(
|
||||||
return Container(
|
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: reactionToEmoji.keys.map((reactionType) {
|
children: reactionToEmoji.keys.map((reactionType) {
|
||||||
var firstUserReaction = message.latestReactions!
|
final firstUserReaction = message.latestReactions!
|
||||||
.firstWhere((element) => element.type == reactionType,
|
.firstWhere((element) => element.type == reactionType,
|
||||||
orElse: () {
|
//ignore: unnecessary_parenthesis
|
||||||
return null;
|
orElse: (() => null) as Reaction Function()?);
|
||||||
} as Reaction Function()?);
|
|
||||||
|
|
||||||
if (firstUserReaction.user == null) {
|
if (firstUserReaction.user == null) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@@ -53,4 +58,3 @@ class UserReactionDisplay extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,23 +3,23 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
/// Launch URL
|
||||||
import 'stream_svg_icon.dart';
|
|
||||||
|
|
||||||
Future<void> launchURL(BuildContext context, String? url) async {
|
Future<void> launchURL(BuildContext context, String? url) async {
|
||||||
if (url != null && await canLaunch(url)) {
|
if (url != null && await canLaunch(url)) {
|
||||||
await launch(url);
|
await launch(url);
|
||||||
} else {
|
} else {
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
Scaffold.of(context).showSnackBar(
|
Scaffold.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text('Cannot launch the url'),
|
content: Text('Cannot launch the url'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows confirmation dialog
|
||||||
Future<bool?> showConfirmationDialog(
|
Future<bool?> showConfirmationDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
@@ -27,14 +27,14 @@ Future<bool?> showConfirmationDialog(
|
|||||||
Widget? icon,
|
Widget? icon,
|
||||||
String? question,
|
String? question,
|
||||||
String? cancelText,
|
String? cancelText,
|
||||||
}) {
|
}) =>
|
||||||
return showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||||
context: context,
|
context: context,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16.0),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
)),
|
)),
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
final effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
||||||
@@ -42,20 +42,20 @@ Future<bool?> showConfirmationDialog(
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 26.0),
|
const SizedBox(height: 26),
|
||||||
if (icon != null) icon,
|
if (icon != null) icon,
|
||||||
SizedBox(height: 26.0),
|
const SizedBox(height: 26),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||||
),
|
),
|
||||||
SizedBox(height: 7.0),
|
const SizedBox(height: 7),
|
||||||
if (question != null)
|
if (question != null)
|
||||||
Text(
|
Text(
|
||||||
question,
|
question,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
SizedBox(height: 36.0),
|
const SizedBox(height: 36),
|
||||||
Container(
|
Container(
|
||||||
color: effect.color!.withOpacity(effect.alpha ?? 1),
|
color: effect.color!.withOpacity(effect.alpha ?? 1),
|
||||||
height: 1,
|
height: 1,
|
||||||
@@ -110,8 +110,8 @@ Future<bool?> showConfirmationDialog(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Shows info dialog
|
||||||
Future<bool?> showInfoDialog(
|
Future<bool?> showInfoDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
@@ -119,44 +119,43 @@ Future<bool?> showInfoDialog(
|
|||||||
Widget? icon,
|
Widget? icon,
|
||||||
String? details,
|
String? details,
|
||||||
StreamChatThemeData? theme,
|
StreamChatThemeData? theme,
|
||||||
}) {
|
}) =>
|
||||||
return showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor:
|
backgroundColor: theme?.colorTheme.white ??
|
||||||
theme?.colorTheme.white ?? StreamChatTheme.of(context).colorTheme.white,
|
StreamChatTheme.of(context).colorTheme.white,
|
||||||
context: context,
|
context: context,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16.0),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16),
|
||||||
)),
|
)),
|
||||||
builder: (context) {
|
builder: (context) => SafeArea(
|
||||||
return SafeArea(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 26.0,
|
height: 26,
|
||||||
),
|
),
|
||||||
if (icon != null) icon,
|
if (icon != null) icon,
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 26.0,
|
height: 26,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: theme?.textTheme.headlineBold ??
|
style: theme?.textTheme.headlineBold ??
|
||||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 7.0,
|
height: 7,
|
||||||
),
|
),
|
||||||
if (details != null) Text(details),
|
if (details != null) Text(details),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 36.0,
|
height: 36,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: theme?.colorTheme.black.withOpacity(.08) ??
|
color: theme?.colorTheme.black.withOpacity(.08) ??
|
||||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||||
height: 1.0,
|
height: 1,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
@@ -175,10 +174,8 @@ Future<bool?> showInfoDialog(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get random png with initials
|
/// Get random png with initials
|
||||||
String getRandomPicUrl(User user) =>
|
String getRandomPicUrl(User user) =>
|
||||||
@@ -236,7 +233,7 @@ String fileSize(dynamic size, [int round = 2]) {
|
|||||||
* the optional parameter [round] specifies the number
|
* the optional parameter [round] specifies the number
|
||||||
* of digits after comma/point (default is 2)
|
* of digits after comma/point (default is 2)
|
||||||
*/
|
*/
|
||||||
final divider = 1024;
|
const divider = 1024;
|
||||||
int _size;
|
int _size;
|
||||||
try {
|
try {
|
||||||
_size = int.parse(size.toString());
|
_size = int.parse(size.toString());
|
||||||
@@ -274,21 +271,21 @@ String fileSize(dynamic size, [int round = 2]) {
|
|||||||
|
|
||||||
if (_size < divider * divider * divider * divider * divider &&
|
if (_size < divider * divider * divider * divider * divider &&
|
||||||
_size % divider == 0) {
|
_size % divider == 0) {
|
||||||
num r = _size / divider / divider / divider / divider;
|
final num r = _size / divider / divider / divider / divider;
|
||||||
return '${r.toStringAsFixed(0)} TB';
|
return '${r.toStringAsFixed(0)} TB';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_size < divider * divider * divider * divider * divider) {
|
if (_size < divider * divider * divider * divider * divider) {
|
||||||
num r = _size / divider / divider / divider / divider;
|
final num r = _size / divider / divider / divider / divider;
|
||||||
return '${r.toStringAsFixed(round)} TB';
|
return '${r.toStringAsFixed(round)} TB';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_size < divider * divider * divider * divider * divider * divider &&
|
if (_size < divider * divider * divider * divider * divider * divider &&
|
||||||
_size % divider == 0) {
|
_size % divider == 0) {
|
||||||
num r = _size / divider / divider / divider / divider / divider;
|
final num r = _size / divider / divider / divider / divider / divider;
|
||||||
return '${r.toStringAsFixed(0)} PB';
|
return '${r.toStringAsFixed(0)} PB';
|
||||||
} else {
|
} else {
|
||||||
num r = _size / divider / divider / divider / divider / divider;
|
final num r = _size / divider / divider / divider / divider / divider;
|
||||||
return '${r.toStringAsFixed(round)} PB';
|
return '${r.toStringAsFixed(round)} PB';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,14 +332,16 @@ StreamSvgIcon getFileTypeImage(String? type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps attachment widget with custom shape
|
||||||
Widget wrapAttachmentWidget(
|
Widget wrapAttachmentWidget(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Widget attachmentWidget,
|
Widget attachmentWidget,
|
||||||
ShapeBorder attachmentShape,
|
ShapeBorder attachmentShape,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
bool reverse,
|
bool reverse,
|
||||||
BorderRadius borderRadius,
|
BorderRadius borderRadius,
|
||||||
) {
|
) =>
|
||||||
return ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: Material(
|
child: Material(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
@@ -355,4 +354,3 @@ Widget wrapAttachmentWidget(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import 'package:synchronized/synchronized.dart';
|
|||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||||
|
|
||||||
|
///
|
||||||
class IVideoService {
|
class IVideoService {
|
||||||
|
IVideoService._();
|
||||||
|
|
||||||
|
/// Singleton instance of [IVideoService]
|
||||||
static final IVideoService instance = IVideoService._();
|
static final IVideoService instance = IVideoService._();
|
||||||
final _lock = Lock();
|
final _lock = Lock();
|
||||||
|
|
||||||
IVideoService._();
|
|
||||||
|
|
||||||
/// compress video from [path]
|
/// compress video from [path]
|
||||||
/// compress video from [path] return [Future<MediaInfo>]
|
/// compress video from [path] return [Future<MediaInfo>]
|
||||||
///
|
///
|
||||||
@@ -26,18 +28,20 @@ class IVideoService {
|
|||||||
/// );
|
/// );
|
||||||
/// debugPrint(info.toJson());
|
/// debugPrint(info.toJson());
|
||||||
/// ```
|
/// ```
|
||||||
Future<MediaInfo?> compressVideo(String? path) async {
|
Future<MediaInfo?> compressVideo(String? path) async => _lock.synchronized(
|
||||||
return _lock.synchronized(() {
|
() => VideoCompress.compressVideo(
|
||||||
return VideoCompress.compressVideo(
|
|
||||||
path!,
|
path!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...).
|
/// Generates a thumbnail image data in memory as UInt8List,
|
||||||
/// The video can be a local video file, or an URL repreents iOS or Android native supported video format.
|
/// it can be easily used by Image.memory(...).
|
||||||
/// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video.
|
/// The video can be a local video file, or an URL repreents iOS or
|
||||||
/// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format.
|
/// Android native supported video format.
|
||||||
|
/// Speicify the maximum height or width for the thumbnail or 0 for
|
||||||
|
/// same resolution as the original video.
|
||||||
|
/// The lower quality value creates lower quality of the thumbnail image,
|
||||||
|
/// but it gets ignored for PNG format.
|
||||||
Future<Uint8List?> generateVideoThumbnail({
|
Future<Uint8List?> generateVideoThumbnail({
|
||||||
required String video,
|
required String video,
|
||||||
ImageFormat imageFormat = ImageFormat.PNG,
|
ImageFormat imageFormat = ImageFormat.PNG,
|
||||||
@@ -45,8 +49,8 @@ class IVideoService {
|
|||||||
int maxWidth = 0,
|
int maxWidth = 0,
|
||||||
int timeMs = 0,
|
int timeMs = 0,
|
||||||
int quality = 10,
|
int quality = 10,
|
||||||
}) {
|
}) =>
|
||||||
return VideoThumbnail.thumbnailData(
|
VideoThumbnail.thumbnailData(
|
||||||
video: video,
|
video: video,
|
||||||
imageFormat: imageFormat,
|
imageFormat: imageFormat,
|
||||||
maxHeight: maxHeight,
|
maxHeight: maxHeight,
|
||||||
@@ -55,7 +59,7 @@ class IVideoService {
|
|||||||
quality: quality,
|
quality: quality,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Get instance of [IVideoService]
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
IVideoService get VideoService => IVideoService.instance;
|
IVideoService get VideoService => IVideoService.instance;
|
||||||
|
|||||||
@@ -4,19 +4,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||||
|
|
||||||
import 'stream_svg_icon.dart';
|
/// Widget for creating video thumbnail image
|
||||||
import 'video_service.dart';
|
|
||||||
|
|
||||||
class VideoThumbnailImage extends StatefulWidget {
|
class VideoThumbnailImage extends StatefulWidget {
|
||||||
final String video;
|
/// Constructor for creating [VideoThumbnailImage]
|
||||||
final double? width;
|
|
||||||
final double? height;
|
|
||||||
final BoxFit? fit;
|
|
||||||
final ImageFormat format;
|
|
||||||
final Widget Function(BuildContext, Object?)? errorBuilder;
|
|
||||||
final WidgetBuilder? placeholderBuilder;
|
|
||||||
|
|
||||||
const VideoThumbnailImage({
|
const VideoThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.video,
|
required this.video,
|
||||||
@@ -28,6 +20,27 @@ class VideoThumbnailImage extends StatefulWidget {
|
|||||||
this.placeholderBuilder,
|
this.placeholderBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Video path
|
||||||
|
final String video;
|
||||||
|
|
||||||
|
/// Width of widget
|
||||||
|
final double? width;
|
||||||
|
|
||||||
|
/// Height of widget
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
/// Fit of iamge
|
||||||
|
final BoxFit? fit;
|
||||||
|
|
||||||
|
/// Image format
|
||||||
|
final ImageFormat format;
|
||||||
|
|
||||||
|
/// Builds widget on error
|
||||||
|
final Widget Function(BuildContext, Object?)? errorBuilder;
|
||||||
|
|
||||||
|
/// Builds placeholder
|
||||||
|
final WidgetBuilder? placeholderBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_VideoThumbnailImageState createState() => _VideoThumbnailImageState();
|
_VideoThumbnailImageState createState() => _VideoThumbnailImageState();
|
||||||
}
|
}
|
||||||
@@ -56,11 +69,9 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => FutureBuilder<Uint8List?>(
|
||||||
return FutureBuilder<Uint8List?>(
|
|
||||||
future: thumbnailFuture,
|
future: thumbnailFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => AnimatedSwitcher(
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 350),
|
duration: const Duration(milliseconds: 350),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot),
|
key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot),
|
||||||
@@ -73,7 +84,7 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
|||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints.expand(),
|
constraints: const BoxConstraints.expand(),
|
||||||
child: widget.placeholderBuilder?.call(context) ??
|
child: widget.placeholderBuilder?.call(context) ??
|
||||||
Shimmer.fromColors(
|
Shimmer.fromColors(
|
||||||
baseColor: StreamChatTheme.of(context)
|
baseColor: StreamChatTheme.of(context)
|
||||||
@@ -97,8 +108,6 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,41 +9,41 @@ environment:
|
|||||||
sdk: '>=2.12.0 <3.0.0'
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
cached_network_image: ^3.0.0
|
||||||
|
characters: ^1.1.0
|
||||||
|
chewie: ^1.0.0
|
||||||
|
collection: ^1.15.0
|
||||||
|
dio: ^4.0.0
|
||||||
|
ezanimation: ^0.5.0
|
||||||
|
file_picker: ^3.0.1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter_core: ^2.0.0-nullsafety.2
|
flutter_keyboard_visibility: ^5.0.1
|
||||||
|
flutter_markdown: ^0.6.1
|
||||||
|
flutter_portal: ^0.4.0
|
||||||
|
flutter_slidable: ^0.6.0
|
||||||
|
flutter_svg: ^0.22.0
|
||||||
|
http_parser: ^4.0.0
|
||||||
|
image_gallery_saver: ^1.6.9
|
||||||
|
image_picker: ^0.7.4
|
||||||
|
jiffy: ^4.1.0
|
||||||
|
lottie: ^1.0.1
|
||||||
|
meta: ^1.3.0
|
||||||
|
path_provider: ^2.0.1
|
||||||
|
photo_manager: ^1.1.4
|
||||||
photo_view: ^0.11.1
|
photo_view: ^0.11.1
|
||||||
rxdart: ^0.26.0
|
rxdart: ^0.26.0
|
||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||||
jiffy: ^4.1.0
|
|
||||||
flutter_svg: ^0.22.0
|
|
||||||
flutter_portal: ^0.4.0
|
|
||||||
cached_network_image: ^3.0.0
|
|
||||||
shimmer: ^2.0.0
|
|
||||||
flutter_markdown: ^0.6.1
|
|
||||||
url_launcher: ^6.0.3
|
|
||||||
video_player: ^2.1.1
|
|
||||||
chewie: ^1.0.0
|
|
||||||
file_picker: ^3.0.1
|
|
||||||
image_picker: ^0.7.4
|
|
||||||
flutter_keyboard_visibility: ^5.0.1
|
|
||||||
video_compress: ^3.0.0
|
|
||||||
visibility_detector: ^0.2.0
|
|
||||||
http_parser: ^4.0.0
|
|
||||||
meta: ^1.3.0
|
|
||||||
lottie: ^1.0.1
|
|
||||||
substring_highlight: ^1.0.26
|
|
||||||
flutter_slidable: ^0.6.0
|
|
||||||
image_gallery_saver: ^1.6.9
|
|
||||||
share_plus: ^2.0.3
|
share_plus: ^2.0.3
|
||||||
photo_manager: ^1.1.4
|
shimmer: ^2.0.0
|
||||||
ezanimation: ^0.5.0
|
stream_chat_flutter_core: ^2.0.0-nullsafety.2
|
||||||
|
substring_highlight: ^1.0.26
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
dio: ^4.0.0
|
url_launcher: ^6.0.3
|
||||||
characters: ^1.1.0
|
video_compress: ^3.0.0
|
||||||
path_provider: ^2.0.1
|
video_player: ^2.1.1
|
||||||
video_thumbnail: ^0.3.3
|
video_thumbnail: ^0.3.3
|
||||||
collection: ^1.15.0
|
visibility_detector: ^0.2.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
@@ -56,7 +56,7 @@ flutter:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
golden_toolkit: ^0.9.0
|
||||||
mocktail: ^0.1.2
|
mocktail: ^0.1.2
|
||||||
pedantic: ^1.11.0
|
pedantic: ^1.11.0
|
||||||
golden_toolkit: ^0.9.0
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class MockAttachmentDownloader extends Mock {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue(MaterialPageRoute(builder: (context) => SizedBox()));
|
registerFallbackValue(
|
||||||
|
MaterialPageRoute(builder: (context) => const SizedBox()));
|
||||||
registerFallbackValue(Message());
|
registerFallbackValue(Message());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
@@ -185,7 +186,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
message: message,
|
message: message,
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
@@ -218,7 +219,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
onShowMessage: onShowMessage,
|
onShowMessage: onShowMessage,
|
||||||
message: Message(
|
message: Message(
|
||||||
@@ -239,7 +240,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.tap(find.text('Show in Chat'));
|
await tester.tap(find.text('Show in Chat'));
|
||||||
verify(() => onShowMessage.call()).called(1);
|
verify(onShowMessage.call).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -250,9 +251,8 @@ void main() {
|
|||||||
final clientState = MockClientState();
|
final clientState = MockClientState();
|
||||||
final mockChannel = MockChannel();
|
final mockChannel = MockChannel();
|
||||||
|
|
||||||
when(() => mockChannel.updateMessage(any())).thenAnswer((_) async {
|
when(() => mockChannel.updateMessage(any()))
|
||||||
return UpdateMessageResponse();
|
.thenAnswer((_) async => UpdateMessageResponse());
|
||||||
});
|
|
||||||
when(() => client.state).thenReturn(clientState);
|
when(() => client.state).thenReturn(clientState);
|
||||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||||
|
|
||||||
@@ -275,12 +275,10 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: mockChannel,
|
channel: mockChannel,
|
||||||
@@ -307,9 +305,8 @@ void main() {
|
|||||||
final clientState = MockClientState();
|
final clientState = MockClientState();
|
||||||
final mockChannel = MockChannel();
|
final mockChannel = MockChannel();
|
||||||
|
|
||||||
when(() => mockChannel.updateMessage(any())).thenAnswer((_) async {
|
when(() => mockChannel.updateMessage(any()))
|
||||||
return UpdateMessageResponse();
|
.thenAnswer((_) async => UpdateMessageResponse());
|
||||||
});
|
|
||||||
when(() => client.state).thenReturn(clientState);
|
when(() => client.state).thenReturn(clientState);
|
||||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||||
|
|
||||||
@@ -328,12 +325,10 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: mockChannel,
|
channel: mockChannel,
|
||||||
@@ -352,15 +347,15 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
'tapping on delete in chat should remove the message if that\'s the only attachment and there is no text',
|
'tapping on delete in chat should remove the message if that\'s the only attachment and there is no text',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final client = MockClient();
|
final client = MockClient();
|
||||||
final clientState = MockClientState();
|
final clientState = MockClientState();
|
||||||
final mockChannel = MockChannel();
|
final mockChannel = MockChannel();
|
||||||
|
|
||||||
when(() => mockChannel.deleteMessage(any())).thenAnswer((_) async {
|
when(() => mockChannel.deleteMessage(any()))
|
||||||
return EmptyResponse();
|
.thenAnswer((_) async => EmptyResponse());
|
||||||
});
|
|
||||||
when(() => client.state).thenReturn(clientState);
|
when(() => client.state).thenReturn(clientState);
|
||||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||||
|
|
||||||
@@ -378,12 +373,10 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: mockChannel,
|
channel: mockChannel,
|
||||||
@@ -412,13 +405,11 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
home: SizedBox(
|
||||||
home: Container(
|
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
imageDownloader: imageDownloader,
|
imageDownloader: imageDownloader,
|
||||||
message: Message(
|
message: Message(
|
||||||
@@ -451,8 +442,8 @@ void main() {
|
|||||||
imageDownloader.progressCallback!(100, 100);
|
imageDownloader.progressCallback!(100, 100);
|
||||||
imageDownloader.completer.complete('path');
|
imageDownloader.completer.complete('path');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byKey(Key('completedIcon')), findsOneWidget);
|
expect(find.byKey(const Key('completedIcon')), findsOneWidget);
|
||||||
await tester.pumpAndSettle(Duration(milliseconds: 500));
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -469,13 +460,11 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
home: SizedBox(
|
||||||
home: Container(
|
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
fileDownloader: fileDownloader,
|
fileDownloader: fileDownloader,
|
||||||
message: Message(
|
message: Message(
|
||||||
@@ -508,8 +497,8 @@ void main() {
|
|||||||
fileDownloader.progressCallback!(100, 100);
|
fileDownloader.progressCallback!(100, 100);
|
||||||
fileDownloader.completer.complete('path');
|
fileDownloader.completer.complete('path');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byKey(Key('completedIcon')), findsOneWidget);
|
expect(find.byKey(const Key('completedIcon')), findsOneWidget);
|
||||||
await tester.pumpAndSettle(Duration(milliseconds: 500));
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ void main() {
|
|||||||
data: streamTheme,
|
data: streamTheme,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: FileAttachment(
|
child: FileAttachment(
|
||||||
size: Size(
|
size: const Size(
|
||||||
300.0,
|
300,
|
||||||
300.0,
|
300,
|
||||||
),
|
),
|
||||||
message: Message(),
|
message: Message(),
|
||||||
attachment: Attachment(
|
attachment: Attachment(
|
||||||
type: 'file',
|
type: 'file',
|
||||||
title: 'example.pdf',
|
title: 'example.pdf',
|
||||||
extraData: {
|
extraData: const {
|
||||||
'mime_type': 'pdf',
|
'mime_type': 'pdf',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,16 +15,14 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: const Material(child: Text('Home')),
|
home: const Material(child: Text('Home')),
|
||||||
routes: <String, WidgetBuilder>{
|
routes: <String, WidgetBuilder>{
|
||||||
'/next': (BuildContext context) {
|
'/next': (BuildContext context) => Material(
|
||||||
return Material(
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: StreamChatTheme(
|
child: StreamChatTheme(
|
||||||
data: StreamChatThemeData.fromTheme(theme),
|
data: StreamChatThemeData.fromTheme(theme),
|
||||||
child: StreamBackButton(),
|
child: const StreamBackButton(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -52,7 +50,7 @@ void main() {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: StreamChatTheme(
|
child: StreamChatTheme(
|
||||||
data: StreamChatThemeData.fromTheme(theme),
|
data: StreamChatThemeData.fromTheme(theme),
|
||||||
child: StreamBackButton(),
|
child: const StreamBackButton(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -78,8 +76,7 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: const Material(child: Text('Home')),
|
home: const Material(child: Text('Home')),
|
||||||
routes: <String, WidgetBuilder>{
|
routes: <String, WidgetBuilder>{
|
||||||
'/next': (BuildContext context) {
|
'/next': (BuildContext context) => Material(
|
||||||
return Material(
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: StreamChatTheme(
|
child: StreamChatTheme(
|
||||||
data: StreamChatThemeData.fromTheme(theme),
|
data: StreamChatThemeData.fromTheme(theme),
|
||||||
@@ -88,8 +85,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -131,7 +127,7 @@ void main() {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: StreamChat(
|
child: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: StreamBackButton(
|
child: const StreamBackButton(
|
||||||
showUnreads: true,
|
showUnreads: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelHeader(),
|
body: ChannelHeader(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -115,7 +115,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelHeader(
|
body: ChannelHeader(
|
||||||
showConnectionStateTile: true,
|
showConnectionStateTile: true,
|
||||||
),
|
),
|
||||||
@@ -178,7 +178,7 @@ void main() {
|
|||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelHeader(
|
body: ChannelHeader(
|
||||||
showConnectionStateTile: true,
|
showConnectionStateTile: true,
|
||||||
),
|
),
|
||||||
@@ -242,7 +242,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelHeader(
|
body: ChannelHeader(
|
||||||
leading: Text('leading'),
|
leading: Text('leading'),
|
||||||
subtitle: Text('subtitle'),
|
subtitle: Text('subtitle'),
|
||||||
@@ -314,11 +314,10 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelHeader(
|
body: ChannelHeader(
|
||||||
showTypingIndicator: false,
|
showTypingIndicator: false,
|
||||||
showBackButton: false,
|
showBackButton: false,
|
||||||
showConnectionStateTile: false,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelImage(),
|
body: ChannelImage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -112,7 +112,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelImage(),
|
body: ChannelImage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -207,7 +207,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelImage(),
|
body: ChannelImage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -248,7 +248,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelImage(
|
body: ChannelImage(
|
||||||
selected: true,
|
selected: true,
|
||||||
),
|
),
|
||||||
@@ -257,7 +257,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(find.byKey(Key('selectedImage')), findsOneWidget);
|
expect(find.byKey(const Key('selectedImage')), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelListHeader(),
|
body: ChannelListHeader(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -51,7 +51,7 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelListHeader(
|
body: ChannelListHeader(
|
||||||
showConnectionStateTile: true,
|
showConnectionStateTile: true,
|
||||||
),
|
),
|
||||||
@@ -80,7 +80,7 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelListHeader(
|
body: ChannelListHeader(
|
||||||
showConnectionStateTile: true,
|
showConnectionStateTile: true,
|
||||||
),
|
),
|
||||||
@@ -111,12 +111,10 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ChannelListHeader(
|
body: ChannelListHeader(
|
||||||
titleBuilder: (context, status, client) {
|
titleBuilder: (context, status, client) => const Text('TITLE'),
|
||||||
return Text('TITLE');
|
subtitle: const Text('SUBTITLE'),
|
||||||
},
|
leading: const Text('LEADING'),
|
||||||
subtitle: Text('SUBTITLE'),
|
actions: const [
|
||||||
leading: Text('LEADING'),
|
|
||||||
actions: [
|
|
||||||
Text('ACTION'),
|
Text('ACTION'),
|
||||||
],
|
],
|
||||||
client: client,
|
client: client,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: ChannelName(),
|
body: ChannelName(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: DeletedMessage(
|
body: DeletedMessage(
|
||||||
messageTheme: MessageTheme(
|
messageTheme: MessageTheme(
|
||||||
createdAt: TextStyle(
|
createdAt: TextStyle(
|
||||||
@@ -81,7 +81,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'deleted_message_light');
|
await screenMatchesGolden(tester, 'deleted_message_light');
|
||||||
@@ -133,7 +133,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'deleted_message_dark');
|
await screenMatchesGolden(tester, 'deleted_message_dark');
|
||||||
@@ -189,7 +189,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'deleted_message_custom');
|
await screenMatchesGolden(tester, 'deleted_message_custom');
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: Portal(
|
body: Portal(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: InfoTile(
|
child: InfoTile(
|
||||||
showMessage: true,
|
showMessage: true,
|
||||||
message: 'message',
|
message: 'message',
|
||||||
@@ -49,9 +49,9 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: Portal(
|
body: Portal(
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: InfoTile(
|
child: InfoTile(
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
message: 'message',
|
message: 'message',
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import 'mocks.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue(MaterialPageRoute(builder: (context) => SizedBox()));
|
registerFallbackValue(
|
||||||
|
MaterialPageRoute(builder: (context) => const SizedBox()));
|
||||||
registerFallbackValue(Message());
|
registerFallbackValue(Message());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
@@ -45,7 +46,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byKey(Key('MessageWidget')), findsOneWidget);
|
expect(find.byKey(const Key('MessageWidget')), findsOneWidget);
|
||||||
expect(find.text('Thread Reply'), findsOneWidget);
|
expect(find.text('Thread Reply'), findsOneWidget);
|
||||||
expect(find.text('Reply'), findsOneWidget);
|
expect(find.text('Reply'), findsOneWidget);
|
||||||
expect(find.text('Edit Message'), findsOneWidget);
|
expect(find.text('Edit Message'), findsOneWidget);
|
||||||
@@ -71,7 +72,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
showEditMessage: false,
|
showEditMessage: false,
|
||||||
showCopyMessage: false,
|
showCopyMessage: false,
|
||||||
@@ -92,7 +93,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byKey(Key('MessageWidget')), findsOneWidget);
|
expect(find.byKey(const Key('MessageWidget')), findsOneWidget);
|
||||||
expect(find.text('Reply'), findsNothing);
|
expect(find.text('Reply'), findsNothing);
|
||||||
expect(find.text('Thread reply'), findsNothing);
|
expect(find.text('Thread reply'), findsNothing);
|
||||||
expect(find.text('Edit message'), findsNothing);
|
expect(find.text('Edit message'), findsNothing);
|
||||||
@@ -120,7 +121,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
@@ -131,8 +132,8 @@ void main() {
|
|||||||
messageTheme: streamTheme.ownMessageTheme,
|
messageTheme: streamTheme.ownMessageTheme,
|
||||||
customActions: [
|
customActions: [
|
||||||
MessageAction(
|
MessageAction(
|
||||||
leading: Icon(Icons.check),
|
leading: const Icon(Icons.check),
|
||||||
title: Text('title'),
|
title: const Text('title'),
|
||||||
onTap: (m) {
|
onTap: (m) {
|
||||||
tapped = true;
|
tapped = true;
|
||||||
},
|
},
|
||||||
@@ -175,7 +176,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
onReplyTap: (m) {
|
onReplyTap: (m) {
|
||||||
tapped = true;
|
tapped = true;
|
||||||
@@ -220,7 +221,7 @@ void main() {
|
|||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
client: client,
|
client: client,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
onThreadReplyTap: (m) {
|
onThreadReplyTap: (m) {
|
||||||
tapped = true;
|
tapped = true;
|
||||||
@@ -260,18 +261,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
@@ -310,22 +309,18 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
editMessageInputBuilder: (context, m) {
|
editMessageInputBuilder: (context, m) => const Text('test'),
|
||||||
return Text('test');
|
|
||||||
},
|
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
user: User(
|
user: User(
|
||||||
@@ -365,18 +360,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
onCopyTap: (m) => tapped = true,
|
onCopyTap: (m) => tapped = true,
|
||||||
message: Message(
|
message: Message(
|
||||||
@@ -416,18 +409,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
status: MessageSendingStatus.failed,
|
status: MessageSendingStatus.failed,
|
||||||
@@ -467,18 +458,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
status: MessageSendingStatus.failed_update,
|
status: MessageSendingStatus.failed_update,
|
||||||
@@ -516,18 +505,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
id: 'testid',
|
id: 'testid',
|
||||||
@@ -575,18 +562,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
id: 'testid',
|
id: 'testid',
|
||||||
@@ -634,18 +619,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
id: 'testid',
|
id: 'testid',
|
||||||
@@ -689,18 +672,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
id: 'testid',
|
id: 'testid',
|
||||||
@@ -748,18 +729,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
builder: (context, child) {
|
builder: (context, child) => StreamChat(
|
||||||
return StreamChat(
|
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: streamTheme,
|
streamChatThemeData: streamTheme,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: StreamChannel(
|
home: StreamChannel(
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
message: Message(
|
message: Message(
|
||||||
id: 'testid',
|
id: 'testid',
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: MessageInput(),
|
body: MessageInput(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -76,7 +76,7 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
expect(find.byType(StreamSvgIcon), findsNWidgets(8));
|
expect(find.byType(StreamSvgIcon), findsNWidgets(8));
|
||||||
expect(find.byKey(Key('messageInputText')), findsOneWidget);
|
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump(Duration(milliseconds: 1000));
|
await tester.pump(const Duration(milliseconds: 1000));
|
||||||
|
|
||||||
expect(find.byType(MessageWidget), findsOneWidget);
|
expect(find.byType(MessageWidget), findsOneWidget);
|
||||||
final messageWidget =
|
final messageWidget =
|
||||||
@@ -95,6 +95,7 @@ void main() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
final onUserAvatarTap = (u) => print('ok');
|
final onUserAvatarTap = (u) => print('ok');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -109,16 +110,16 @@ void main() {
|
|||||||
showUserAvatar: DisplayWidget.gone,
|
showUserAvatar: DisplayWidget.gone,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
attachmentBorderRadiusGeometry: BorderRadius.circular(1),
|
attachmentBorderRadiusGeometry: BorderRadius.circular(1),
|
||||||
attachmentShape: RoundedRectangleBorder(),
|
attachmentShape: const RoundedRectangleBorder(),
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
messageShape: RoundedRectangleBorder(),
|
messageShape: const RoundedRectangleBorder(),
|
||||||
onUserAvatarTap: onUserAvatarTap,
|
onUserAvatarTap: onUserAvatarTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump(Duration(milliseconds: 1000));
|
await tester.pump(const Duration(milliseconds: 1000));
|
||||||
|
|
||||||
expect(find.byType(MessageWidget), findsOneWidget);
|
expect(find.byType(MessageWidget), findsOneWidget);
|
||||||
final messageWidget =
|
final messageWidget =
|
||||||
@@ -130,8 +131,8 @@ void main() {
|
|||||||
expect(messageWidget.showUserAvatar, DisplayWidget.gone);
|
expect(messageWidget.showUserAvatar, DisplayWidget.gone);
|
||||||
expect(messageWidget.reverse, true);
|
expect(messageWidget.reverse, true);
|
||||||
expect(messageWidget.showReactions, false);
|
expect(messageWidget.showReactions, false);
|
||||||
expect(messageWidget.attachmentShape, RoundedRectangleBorder());
|
expect(messageWidget.attachmentShape, const RoundedRectangleBorder());
|
||||||
expect(messageWidget.shape, RoundedRectangleBorder());
|
expect(messageWidget.shape, const RoundedRectangleBorder());
|
||||||
expect(messageWidget.showReactions, false);
|
expect(messageWidget.showReactions, false);
|
||||||
expect(messageWidget.showUsername, false);
|
expect(messageWidget.showUsername, false);
|
||||||
expect(messageWidget.showThreadReplyIndicator, false);
|
expect(messageWidget.showThreadReplyIndicator, false);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class MockChannel extends Mock implements Channel {
|
|||||||
Future<bool> get initialized async => true;
|
Future<bool> get initialized async => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
// ignore: prefer_expression_function_bodies
|
||||||
Future<void> keyStroke([String? parentId]) async {
|
Future<void> keyStroke([String? parentId]) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ void main() {
|
|||||||
SimpleFrame(
|
SimpleFrame(
|
||||||
child: StreamChatTheme(
|
child: StreamChatTheme(
|
||||||
data: StreamChatThemeData(),
|
data: StreamChatThemeData(),
|
||||||
child: Container(
|
child: const SizedBox(
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
reactions: [],
|
reactions: [],
|
||||||
borderColor: Colors.black,
|
borderColor: Colors.black,
|
||||||
@@ -26,7 +26,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(100, 100),
|
surfaceSize: const Size(100, 100),
|
||||||
);
|
);
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_0');
|
await screenMatchesGolden(tester, 'reaction_bubble_0');
|
||||||
},
|
},
|
||||||
@@ -47,7 +47,7 @@ void main() {
|
|||||||
StreamChat(
|
StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: theme,
|
streamChatThemeData: theme,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
reactions: [
|
reactions: [
|
||||||
Reaction(
|
Reaction(
|
||||||
@@ -61,7 +61,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(100, 100),
|
surfaceSize: const Size(100, 100),
|
||||||
);
|
);
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_like_light');
|
await screenMatchesGolden(tester, 'reaction_bubble_like_light');
|
||||||
},
|
},
|
||||||
@@ -97,7 +97,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(100, 100),
|
surfaceSize: const Size(100, 100),
|
||||||
);
|
);
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_like_dark');
|
await screenMatchesGolden(tester, 'reaction_bubble_like_dark');
|
||||||
},
|
},
|
||||||
@@ -141,7 +141,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(140, 140),
|
surfaceSize: const Size(140, 140),
|
||||||
);
|
);
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_3_light');
|
await screenMatchesGolden(tester, 'reaction_bubble_3_light');
|
||||||
},
|
},
|
||||||
@@ -185,7 +185,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(140, 140),
|
surfaceSize: const Size(140, 140),
|
||||||
);
|
);
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_3_dark');
|
await screenMatchesGolden(tester, 'reaction_bubble_3_dark');
|
||||||
},
|
},
|
||||||
@@ -205,7 +205,7 @@ void main() {
|
|||||||
StreamChat(
|
StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
reactions: [
|
reactions: [
|
||||||
Reaction(
|
Reaction(
|
||||||
@@ -224,14 +224,13 @@ void main() {
|
|||||||
borderColor: Colors.red,
|
borderColor: Colors.red,
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
maskColor: Colors.green,
|
maskColor: Colors.green,
|
||||||
highlightOwnReactions: true,
|
|
||||||
reverse: true,
|
reverse: true,
|
||||||
flipTail: true,
|
flipTail: true,
|
||||||
tailCirclesSpacing: 4,
|
tailCirclesSpacing: 4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size(200, 200),
|
surfaceSize: const Size(200, 200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'reaction_bubble_2');
|
await screenMatchesGolden(tester, 'reaction_bubble_2');
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SimpleFrame extends StatelessWidget {
|
class SimpleFrame extends StatelessWidget {
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const SimpleFrame({Key? key, required this.child}) : super(key: key);
|
const SimpleFrame({Key? key, required this.child}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Container(
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFFFFFF),
|
color: const Color(0xFFFFFFFF),
|
||||||
@@ -16,4 +15,3 @@ class SimpleFrame extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'system_message_light');
|
await screenMatchesGolden(tester, 'system_message_light');
|
||||||
@@ -153,7 +153,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screenMatchesGolden(tester, 'system_message_dark');
|
await screenMatchesGolden(tester, 'system_message_dark');
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ void main() {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ThreadHeader(
|
body: ThreadHeader(
|
||||||
parent: Message(),
|
parent: Message(),
|
||||||
subtitle: Text('subtitle'),
|
subtitle: const Text('subtitle'),
|
||||||
leading: Text('leading'),
|
leading: const Text('leading'),
|
||||||
title: Text('title'),
|
title: const Text('title'),
|
||||||
onTitleTap: () {
|
onTitleTap: () {
|
||||||
tapped = true;
|
tapped = true;
|
||||||
},
|
},
|
||||||
actions: [
|
actions: const [
|
||||||
Text('action'),
|
Text('action'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: TypingIndicator(),
|
body: TypingIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(find.byKey(Key('typings')), findsOneWidget);
|
expect(find.byKey(const Key('typings')), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ void main() {
|
|||||||
client: client,
|
client: client,
|
||||||
child: StreamChannel(
|
child: StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Scaffold(
|
child: const Scaffold(
|
||||||
body: UnreadIndicator(),
|
body: UnreadIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -357,9 +357,9 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
var message = snapshot.error.toString();
|
var message = snapshot.error.toString();
|
||||||
if (snapshot.error is DioError) {
|
if (snapshot.error is DioError) {
|
||||||
final dioError = snapshot.error as DioError;
|
final dioError = snapshot.error as DioError?;
|
||||||
if (dioError.type == DioErrorType.response) {
|
if (dioError?.type == DioErrorType.response) {
|
||||||
message = dioError.message;
|
message = dioError!.message;
|
||||||
} else {
|
} else {
|
||||||
message = 'Check your connection and retry';
|
message = 'Check your connection and retry';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
- lib/**/*.g.dart
|
|
||||||
- lib/**/*.freezed.dart
|
|
||||||
- example/*
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
- always_use_package_imports
|
|
||||||
- avoid_empty_else
|
|
||||||
- avoid_relative_lib_imports
|
|
||||||
- avoid_slow_async_io
|
|
||||||
- avoid_types_as_parameter_names
|
|
||||||
- cancel_subscriptions
|
|
||||||
- close_sinks
|
|
||||||
- control_flow_in_finally
|
|
||||||
- diagnostic_describe_all_properties
|
|
||||||
- empty_statements
|
|
||||||
- hash_and_equals
|
|
||||||
- invariant_booleans
|
|
||||||
- iterable_contains_unrelated_type
|
|
||||||
- list_remove_unrelated_type
|
|
||||||
- literal_only_boolean_expressions
|
|
||||||
- no_adjacent_strings_in_list
|
|
||||||
- no_duplicate_case_values
|
|
||||||
- no_logic_in_create_state
|
|
||||||
- prefer_void_to_null
|
|
||||||
- test_types_in_equals
|
|
||||||
- throw_in_finally
|
|
||||||
- unnecessary_statements
|
|
||||||
- unrelated_type_equality_checks
|
|
||||||
- omit_local_variable_types
|
|
||||||
- use_key_in_widget_constructors
|
|
||||||
- valid_regexps
|
|
||||||
- always_declare_return_types
|
|
||||||
- always_put_required_named_parameters_first
|
|
||||||
- always_require_non_null_named_parameters
|
|
||||||
- annotate_overrides
|
|
||||||
- avoid_bool_literals_in_conditional_expressions
|
|
||||||
- avoid_catching_errors
|
|
||||||
- avoid_init_to_null
|
|
||||||
- avoid_null_checks_in_equality_operators
|
|
||||||
- avoid_positional_boolean_parameters
|
|
||||||
- avoid_private_typedef_functions
|
|
||||||
- avoid_redundant_argument_values
|
|
||||||
- avoid_return_types_on_setters
|
|
||||||
- avoid_returning_null_for_void
|
|
||||||
- avoid_shadowing_type_parameters
|
|
||||||
- avoid_single_cascade_in_expression_statements
|
|
||||||
- avoid_unnecessary_containers
|
|
||||||
- avoid_unused_constructor_parameters
|
|
||||||
- await_only_futures
|
|
||||||
- camel_case_extensions
|
|
||||||
- camel_case_types
|
|
||||||
- cascade_invocations
|
|
||||||
|
|
||||||
- constant_identifier_names
|
|
||||||
- curly_braces_in_flow_control_structures
|
|
||||||
- directives_ordering
|
|
||||||
- empty_catches
|
|
||||||
- empty_constructor_bodies
|
|
||||||
- exhaustive_cases
|
|
||||||
- file_names
|
|
||||||
- implementation_imports
|
|
||||||
- join_return_with_assignment
|
|
||||||
- leading_newlines_in_multiline_strings
|
|
||||||
- library_names
|
|
||||||
- library_prefixes
|
|
||||||
- lines_longer_than_80_chars
|
|
||||||
- missing_whitespace_between_adjacent_strings
|
|
||||||
- non_constant_identifier_names
|
|
||||||
- null_closures
|
|
||||||
- one_member_abstracts
|
|
||||||
- only_throw_errors
|
|
||||||
- package_api_docs
|
|
||||||
- package_prefixed_library_names
|
|
||||||
- parameter_assignments
|
|
||||||
- prefer_adjacent_string_concatenation
|
|
||||||
- prefer_asserts_in_initializer_lists
|
|
||||||
- prefer_asserts_with_message
|
|
||||||
- prefer_collection_literals
|
|
||||||
- prefer_conditional_assignment
|
|
||||||
- prefer_const_constructors
|
|
||||||
- prefer_const_constructors_in_immutables
|
|
||||||
- prefer_const_declarations
|
|
||||||
- prefer_const_literals_to_create_immutables
|
|
||||||
- prefer_constructors_over_static_methods
|
|
||||||
- prefer_contains
|
|
||||||
- prefer_equal_for_default_values
|
|
||||||
- prefer_expression_function_bodies
|
|
||||||
- prefer_final_fields
|
|
||||||
- prefer_final_in_for_each
|
|
||||||
- prefer_final_locals
|
|
||||||
- prefer_function_declarations_over_variables
|
|
||||||
- prefer_generic_function_type_aliases
|
|
||||||
- prefer_if_elements_to_conditional_expressions
|
|
||||||
- prefer_if_null_operators
|
|
||||||
- prefer_initializing_formals
|
|
||||||
- prefer_inlined_adds
|
|
||||||
- prefer_int_literals
|
|
||||||
- prefer_interpolation_to_compose_strings
|
|
||||||
- prefer_is_empty
|
|
||||||
- prefer_is_not_empty
|
|
||||||
- prefer_is_not_operator
|
|
||||||
- prefer_null_aware_operators
|
|
||||||
- prefer_single_quotes
|
|
||||||
- prefer_spread_collections
|
|
||||||
- prefer_typing_uninitialized_variables
|
|
||||||
- provide_deprecation_message
|
|
||||||
- public_member_api_docs
|
|
||||||
- recursive_getters
|
|
||||||
- sized_box_for_whitespace
|
|
||||||
- slash_for_doc_comments
|
|
||||||
- sort_child_properties_last
|
|
||||||
- sort_constructors_first
|
|
||||||
- sort_unnamed_constructors_first
|
|
||||||
|
|
||||||
- type_annotate_public_apis
|
|
||||||
- type_init_formals
|
|
||||||
- unnecessary_await_in_return
|
|
||||||
- unnecessary_brace_in_string_interps
|
|
||||||
- unnecessary_const
|
|
||||||
- unnecessary_getters_setters
|
|
||||||
- unnecessary_lambdas
|
|
||||||
- unnecessary_new
|
|
||||||
- unnecessary_null_aware_assignments
|
|
||||||
- unnecessary_null_in_if_null_operators
|
|
||||||
- unnecessary_nullable_for_final_variable_declarations
|
|
||||||
- unnecessary_parenthesis
|
|
||||||
- unnecessary_raw_strings
|
|
||||||
- unnecessary_string_escapes
|
|
||||||
- unnecessary_string_interpolations
|
|
||||||
- unnecessary_this
|
|
||||||
- use_is_even_rather_than_modulo
|
|
||||||
- use_late_for_private_fields_and_variables
|
|
||||||
- use_rethrow_when_possible
|
|
||||||
- use_setters_to_change_properties
|
|
||||||
- use_to_and_as_if_applicable
|
|
||||||
- package_names
|
|
||||||
- sort_pub_dependencies
|
|
||||||
|
|
||||||
# To be added when null-safe:
|
|
||||||
# - cast_nullable_to_non_nullable
|
|
||||||
#- unnecessary_null_checks
|
|
||||||
# - tighten_type_of_initializing_formals
|
|
||||||
# - null_check_on_nullable_type_parameter
|
|
||||||
Reference in New Issue
Block a user