Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/mention-screen
Conflicts: lib/src/message_list_view.dart
@@ -1,63 +1,63 @@
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- extension-methods
|
||||
exclude:
|
||||
- lib/**/*.g.dart
|
||||
- example/**
|
||||
|
||||
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
|
||||
#include: package:pedantic/analysis_options.yaml
|
||||
#
|
||||
#analyzer:
|
||||
# enable-experiment:
|
||||
# - extension-methods
|
||||
# exclude:
|
||||
# - lib/**/*.g.dart
|
||||
# - example/**
|
||||
#
|
||||
#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
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -1 +1 @@
|
||||
13a41b8138c4868054e44b5158f3bdd6
|
||||
c770358a10272b4ee1cdc2eb432445f6
|
||||
@@ -265,7 +265,7 @@ SPEC CHECKSUMS:
|
||||
FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
|
||||
FirebaseInstanceID: bd3ffc24367f901a43c063b36c640b345a4a5dd1
|
||||
FirebaseMessaging: 5eca4ef173de76253352511aafef774caa1cba2a
|
||||
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
|
||||
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
|
||||
flutter_apns: ddc629f26016140bf52165040b0a8e8869f9ce32
|
||||
flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
|
||||
@@ -314,7 +314,6 @@
|
||||
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
|
||||
"${PODS_ROOT}/../Flutter/Flutter.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
|
||||
@@ -348,7 +347,6 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
|
||||
|
||||
@@ -58,8 +58,6 @@
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -5,27 +5,39 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00094">
|
||||
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00047">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000521">
|
||||
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000176">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000605">
|
||||
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000168">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: is_ci" time="0.000675">
|
||||
<testcase classname="fastlane.lanes" name="3: Switch to ios match_me lane" time="0.000157">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: match" time="48.727951">
|
||||
<testcase classname="fastlane.lanes" name="4: is_ci" time="0.000161">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: match" time="32.299713">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="6: gym" time="9.737819">
|
||||
|
||||
<failure message="/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:28:in `block (2 levels) in parsing_binding' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane.rb:33:in `call' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/bin/fastlane:23:in `<top (required)>' /usr/local/bin/fastlane:23:in `load' /usr/local/bin/fastlane:23:in `<top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:30:in `block in <top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:22:in `<top (required)>' /usr/local/bin/bundle:23:in `load' /usr/local/bin/bundle:23:in `<main>' Error building the application - see the log above" />
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'dart:io';
|
||||
import 'package:example/stream_version.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'choose_user_page.dart';
|
||||
import 'main.dart';
|
||||
import 'notifications_service.dart';
|
||||
import 'choose_user_page.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class AdvancedOptionsPage extends StatefulWidget {
|
||||
@override
|
||||
@@ -34,20 +34,23 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
resizeToAvoidBottomPadding: false,
|
||||
appBar: AppBar(
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
brightness: Theme.of(context).brightness,
|
||||
title: Text(
|
||||
'Advanced Options',
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.headlineBold
|
||||
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
@@ -57,15 +60,11 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
0,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _apiKeyController,
|
||||
@@ -87,265 +86,250 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _apiKeyError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
? StreamChatTheme.of(context).colorTheme.accentRed
|
||||
: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
fillColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
filled: true,
|
||||
labelText:
|
||||
'Chat API Key ${_apiKeyError != null ? ':$_apiKeyError' : ''}',
|
||||
'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}',
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextFormField(
|
||||
controller: _userIdController,
|
||||
onChanged: (_) {
|
||||
if (_userIdError != null) {
|
||||
setState(() {
|
||||
_userIdError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
setState(() {
|
||||
_userIdError = 'Please enter the User ID';
|
||||
});
|
||||
return _userIdError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _userIdController,
|
||||
onChanged: (_) {
|
||||
if (_userIdError != null) {
|
||||
setState(() {
|
||||
_userIdError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
setState(() {
|
||||
_userIdError = 'Please enter the User ID';
|
||||
});
|
||||
return _userIdError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
color: _userIdError != null
|
||||
? StreamChatTheme.of(context).colorTheme.accentRed
|
||||
: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: _userIdError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText:
|
||||
'User ID ${_userIdError != null ? ':$_userIdError' : ''}',
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
filled: true,
|
||||
labelText:
|
||||
'User ID ${_userIdError != null ? ': $_userIdError' : ''}',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextFormField(
|
||||
onChanged: (_) {
|
||||
if (_userTokenError != null) {
|
||||
setState(() {
|
||||
_userTokenError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
controller: _userTokenController,
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
setState(() {
|
||||
_userTokenError = 'Please enter the user token';
|
||||
});
|
||||
return _userTokenError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
SizedBox(height: 8),
|
||||
TextFormField(
|
||||
onChanged: (_) {
|
||||
if (_userTokenError != null) {
|
||||
setState(() {
|
||||
_userTokenError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
controller: _userTokenController,
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
setState(() {
|
||||
_userTokenError = 'Please enter the user token';
|
||||
});
|
||||
return _userTokenError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
color: _userTokenError != null
|
||||
? StreamChatTheme.of(context).colorTheme.accentRed
|
||||
: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: _userTokenError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText:
|
||||
'User Token ${_userTokenError != null ? ':$_userTokenError' : ''}',
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
filled: true,
|
||||
labelText:
|
||||
'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextFormField(
|
||||
controller: _usernameController,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText: 'Username (optional)',
|
||||
SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _usernameController,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
fillColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
filled: true,
|
||||
labelText: 'Username (optional)',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: FlatButton(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
minWidth: double.infinity,
|
||||
height: 48,
|
||||
child: Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (_formKey.currentState.validate()) {
|
||||
final apiKey = _apiKeyController.text;
|
||||
final userId = _userIdController.text;
|
||||
final userToken = _userTokenController.text;
|
||||
final username = _usernameController.text;
|
||||
Spacer(),
|
||||
RaisedButton(
|
||||
elevation: 0,
|
||||
child: Text('Login', style: TextStyle(fontSize: 16)),
|
||||
onPressed: () async {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (_formKey.currentState.validate()) {
|
||||
final apiKey = _apiKeyController.text;
|
||||
final userId = _userIdController.text;
|
||||
final userToken = _userTokenController.text;
|
||||
final username = _usernameController.text;
|
||||
|
||||
loading = true;
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white,
|
||||
),
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
loading = true;
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
barrierColor:
|
||||
StreamChatTheme.of(context).colorTheme.overlay,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
);
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = Client(
|
||||
apiKey,
|
||||
logLevel: Level.INFO,
|
||||
showLocalNotification:
|
||||
(!kIsWeb && Platform.isAndroid)
|
||||
? showLocalNotification
|
||||
: null,
|
||||
persistenceEnabled: true,
|
||||
);
|
||||
final client = Client(
|
||||
apiKey,
|
||||
logLevel: Level.INFO,
|
||||
showLocalNotification: (!kIsWeb && Platform.isAndroid)
|
||||
? showLocalNotification
|
||||
: null,
|
||||
persistenceEnabled: true,
|
||||
);
|
||||
|
||||
try {
|
||||
await client.setUser(
|
||||
User(id: userId, extraData: {
|
||||
'name': username,
|
||||
}),
|
||||
userToken,
|
||||
);
|
||||
try {
|
||||
await client.setUser(
|
||||
User(id: userId, extraData: {
|
||||
'name': username,
|
||||
}),
|
||||
userToken,
|
||||
);
|
||||
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
secureStorage.write(
|
||||
key: kStreamApiKey,
|
||||
value: apiKey,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamUserId,
|
||||
value: userId,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamToken,
|
||||
value: userToken,
|
||||
);
|
||||
} catch (e) {
|
||||
var errorText = 'Error connecting, retry';
|
||||
if (e is Map) {
|
||||
errorText = e['message'] ?? errorText;
|
||||
}
|
||||
Navigator.pop(context);
|
||||
setState(() {
|
||||
_apiKeyError = errorText;
|
||||
});
|
||||
loading = false;
|
||||
await client.disconnect();
|
||||
return;
|
||||
}
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
secureStorage.write(
|
||||
key: kStreamApiKey,
|
||||
value: apiKey,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamUserId,
|
||||
value: userId,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamToken,
|
||||
value: userToken,
|
||||
);
|
||||
} catch (e) {
|
||||
var errorText = 'Error connecting, retry';
|
||||
if (e is Map) {
|
||||
errorText = e['message'] ?? errorText;
|
||||
}
|
||||
Navigator.pop(context);
|
||||
setState(() {
|
||||
_apiKeyError = errorText;
|
||||
});
|
||||
loading = false;
|
||||
await client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
//TODO change to system once dark theme is implemented
|
||||
themeMode: ThemeMode.light,
|
||||
builder: (context, widget) {
|
||||
return StreamChat(
|
||||
child: widget,
|
||||
client: client,
|
||||
);
|
||||
},
|
||||
home: ChannelListPage(),
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
builder: (context, widget) {
|
||||
return StreamChat(
|
||||
child: widget,
|
||||
client: client,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
home: ChannelListPage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
StreamVersion(),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,10 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'main.dart';
|
||||
import 'routes/routes.dart';
|
||||
|
||||
/// Detail screen for a 1:1 chat correspondence
|
||||
class ChatInfoScreen extends StatefulWidget {
|
||||
@@ -21,16 +24,18 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFe6e6e6),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
body: ListView(
|
||||
children: [
|
||||
_buildUserHeader(),
|
||||
SizedBox(
|
||||
Container(
|
||||
height: 8.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
_buildOptionListTiles(),
|
||||
SizedBox(
|
||||
Container(
|
||||
height: 8.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
if ([
|
||||
'admin',
|
||||
@@ -47,7 +52,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
|
||||
Widget _buildUserHeader() {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -74,14 +79,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
SizedBox(height: 7.0),
|
||||
_buildConnectedTitleState(),
|
||||
SizedBox(height: 15.0),
|
||||
_OptionListTile(
|
||||
OptionListTile(
|
||||
title: '@${widget.user.id}',
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
widget.user.name,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5), fontSize: 16.0),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
onTap: () {},
|
||||
@@ -92,7 +102,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
top: 21,
|
||||
left: 16,
|
||||
child: InkWell(
|
||||
child: StreamSvgIcon.left(),
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -113,7 +125,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
// title: 'Notifications',
|
||||
// leading: StreamSvgIcon.Icon_notification(
|
||||
// size: 24.0,
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
// ),
|
||||
// trailing: CupertinoSwitch(
|
||||
// value: true,
|
||||
@@ -124,11 +136,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
StreamBuilder<bool>(
|
||||
stream: StreamChannel.of(context).channel.isMutedStream,
|
||||
builder: (context, snapshot) {
|
||||
return _OptionListTile(
|
||||
return OptionListTile(
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
title: 'Mute user',
|
||||
leading: StreamSvgIcon.mute(
|
||||
size: 23.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 22.0),
|
||||
child: StreamSvgIcon.mute(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: snapshot.data == null
|
||||
? CircularProgressIndicator()
|
||||
@@ -149,7 +169,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
// title: 'Block User',
|
||||
// leading: StreamSvgIcon.Icon_user_delete(
|
||||
// size: 24.0,
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
// ),
|
||||
// trailing: CupertinoSwitch(
|
||||
// value: widget.user.banned,
|
||||
@@ -163,37 +183,118 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
// ),
|
||||
// onTap: () {},
|
||||
// ),
|
||||
_OptionListTile(
|
||||
OptionListTile(
|
||||
title: 'Photos & Videos',
|
||||
leading: StreamSvgIcon.pictures(
|
||||
size: 32.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.pictures(
|
||||
size: 36.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageSearchBloc(
|
||||
child: ChannelMediaDisplayScreen(
|
||||
sortOptions: [
|
||||
SortOption(
|
||||
'created_at',
|
||||
direction: SortOption.ASC,
|
||||
),
|
||||
],
|
||||
paginationParams: PaginationParams(limit: 20),
|
||||
onShowMessage: (m, c) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
final message = m;
|
||||
final channel = client.channel(
|
||||
c.type,
|
||||
id: c.id,
|
||||
);
|
||||
if (channel.state == null) {
|
||||
await channel.watch();
|
||||
}
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
arguments: ChannelPageArgs(
|
||||
channel: channel,
|
||||
initialMessage: message,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionListTile(
|
||||
OptionListTile(
|
||||
title: 'Files',
|
||||
leading: StreamSvgIcon.files(
|
||||
size: 32.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: StreamSvgIcon.files(
|
||||
size: 32.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageSearchBloc(
|
||||
child: ChannelFileDisplayScreen(
|
||||
sortOptions: [
|
||||
SortOption(
|
||||
'created_at',
|
||||
direction: SortOption.ASC,
|
||||
),
|
||||
],
|
||||
paginationParams: PaginationParams(limit: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionListTile(
|
||||
OptionListTile(
|
||||
title: 'Shared groups',
|
||||
leading: StreamSvgIcon.Icon_group(
|
||||
size: 24.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 22.0),
|
||||
child: StreamSvgIcon.Icon_group(
|
||||
size: 24.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -207,16 +308,23 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
}
|
||||
|
||||
Widget _buildDeleteListTile() {
|
||||
return _OptionListTile(
|
||||
title: 'Delete',
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
size: 24.0,
|
||||
return OptionListTile(
|
||||
title: 'Delete Conversation',
|
||||
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 22.0),
|
||||
child: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_showDeleteDialog();
|
||||
},
|
||||
titleColor: Colors.red,
|
||||
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +336,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
question: 'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
@@ -248,12 +356,20 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
if (otherMember.online) {
|
||||
alternativeWidget = Text(
|
||||
'Online',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
);
|
||||
} else {
|
||||
alternativeWidget = Text(
|
||||
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -272,10 +388,10 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
),
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
color: Color(0xff20E070),
|
||||
color: StreamChatTheme.of(context).colorTheme.accentGreen,
|
||||
),
|
||||
),
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
alternativeWidget,
|
||||
],
|
||||
@@ -283,72 +399,6 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionListTile extends StatelessWidget {
|
||||
final String title;
|
||||
final StreamSvgIcon leading;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final Color titleColor;
|
||||
|
||||
_OptionListTile({
|
||||
this.title,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.titleColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Color(0xffe6e6e6),
|
||||
height: 2.0,
|
||||
),
|
||||
Material(
|
||||
color: Colors.white,
|
||||
child: Container(
|
||||
height: 56.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null)
|
||||
Expanded(
|
||||
child: Center(child: leading),
|
||||
),
|
||||
if (leading == null)
|
||||
SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: titleColor),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: trailing ?? Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SharedGroupsScreen extends StatefulWidget {
|
||||
final User mainUser;
|
||||
final User otherUser;
|
||||
@@ -365,14 +415,16 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
var chat = StreamChat.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Shared Groups',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
@@ -381,7 +433,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
@@ -389,7 +441,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
body: FutureBuilder<List<Channel>>(
|
||||
future: chat.client.queryChannels(
|
||||
@@ -409,12 +461,46 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
},
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.message(
|
||||
size: 136.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Text(
|
||||
'No Shared Groups',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Group shared with User will appear here.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (context, position) {
|
||||
@@ -488,7 +574,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${channel.memberCount} members',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -496,7 +586,8 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
),
|
||||
Container(
|
||||
height: 1.0,
|
||||
color: Color(0xffe6e6e6),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -504,71 +595,3 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MediaDisplayScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Photos & Videos',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FileDisplayScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Files',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
|
||||
onTap: _pauseItemAddition ? resumeItemAddition : null,
|
||||
child: Material(
|
||||
elevation: 1,
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
@@ -78,10 +78,14 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(
|
||||
'TO:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
@@ -111,10 +115,14 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
|
||||
disabledBorder: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.only(top: 4.0),
|
||||
hintText: widget.hint,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 14,
|
||||
),
|
||||
hintStyle: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -126,11 +134,17 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
|
||||
child: IconButton(
|
||||
icon: _chips.isEmpty
|
||||
? StreamSvgIcon.user(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
size: 24,
|
||||
)
|
||||
: StreamSvgIcon.userAdd(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
size: 24,
|
||||
),
|
||||
onPressed: resumeItemAddition,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:example/advanced_options_page.dart';
|
||||
import 'package:example/main.dart';
|
||||
import 'package:example/stream_version.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -60,6 +58,7 @@ class ChooseUserPage extends StatelessWidget {
|
||||
(_, value) => value..extraData['image'] = getRandomPicUrl(value));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -73,6 +72,7 @@ class ChooseUserPage extends StatelessWidget {
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
height: 40,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -80,19 +80,12 @@ class ChooseUserPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(bottom: 13.0),
|
||||
child: Text(
|
||||
'Welcome to Stream Chat',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.title,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Select a user to try the Flutter SDK:',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.body,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
@@ -100,9 +93,8 @@ class ChooseUserPage extends StatelessWidget {
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, i) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Colors.black12,
|
||||
height: 1,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
);
|
||||
},
|
||||
itemCount: users.length + 1,
|
||||
@@ -116,11 +108,16 @@ class ChooseUserPage extends StatelessWidget {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.overlay,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
height: 100,
|
||||
width: 100,
|
||||
@@ -169,13 +166,24 @@ class ChooseUserPage extends StatelessWidget {
|
||||
),
|
||||
title: Text(
|
||||
user.name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style:
|
||||
StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
),
|
||||
subtitle: Text('Stream test account'),
|
||||
trailing: SvgPicture.asset(
|
||||
'assets/icon_arrow_right.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
subtitle: Text(
|
||||
'Stream test account',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey,
|
||||
),
|
||||
),
|
||||
trailing: StreamSvgIcon.arrow_right(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -185,16 +193,26 @@ class ChooseUserPage extends StatelessWidget {
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
child: StreamSvgIcon.settings(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).secondaryColor,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyWhisper,
|
||||
),
|
||||
title: Text(
|
||||
'Advanced Options',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
),
|
||||
subtitle: Text(
|
||||
'Custom settings',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
subtitle: Text('Custom settings'),
|
||||
trailing: SvgPicture.asset(
|
||||
'assets/icon_arrow_right.svg',
|
||||
height: 24,
|
||||
|
||||
@@ -39,27 +39,29 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ThemeData(
|
||||
primarySwatch: Colors.green,
|
||||
);
|
||||
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
builder: (context, child) => StreamChat(
|
||||
child: child,
|
||||
client: client,
|
||||
streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
|
||||
ownMessageTheme: MessageTheme(
|
||||
messageBackgroundColor: Colors.black,
|
||||
messageText: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
final themeData = ThemeData(primarySwatch: Colors.green);
|
||||
final defaultTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
final customTheme = defaultTheme.merge(StreamChatThemeData(
|
||||
ownMessageTheme: MessageTheme(
|
||||
messageBackgroundColor: StreamChatTheme.of(context).colorTheme.black,
|
||||
messageText: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
return MaterialApp(
|
||||
theme: themeData,
|
||||
builder: (context, child) {
|
||||
return StreamChat(
|
||||
child: child,
|
||||
client: client,
|
||||
streamChatThemeData: customTheme,
|
||||
);
|
||||
},
|
||||
home: ChannelListPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,10 +84,12 @@ class ChannelListPage extends StatelessWidget {
|
||||
channel: channel,
|
||||
),
|
||||
title: ChannelName(
|
||||
channel: channel,
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.title.copyWith(
|
||||
color: Colors.black.withOpacity(opacity),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(opacity),
|
||||
),
|
||||
),
|
||||
subtitle: Text(subtitle),
|
||||
|
||||
@@ -127,7 +127,6 @@ class ChannelPage extends StatelessWidget {
|
||||
showTimestamp: !details.isNextUser,
|
||||
showUsername: !details.isNextUser,
|
||||
showReactions: false,
|
||||
showReplyIndicator: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +59,16 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
leading: const StreamBackButton(),
|
||||
title: Text(
|
||||
'Name of Group Chat',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
@@ -82,7 +83,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
'NAME',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
@@ -99,7 +100,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
hintText: 'Choose a group chat name',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14, color: Colors.black.withOpacity(.5)),
|
||||
fontSize: 14,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -113,7 +116,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.check(
|
||||
size: 24,
|
||||
color: _isGroupNameEmpty ? Colors.grey : Color(0xFF006CFF),
|
||||
color: _isGroupNameEmpty
|
||||
? StreamChatTheme.of(context).colorTheme.grey
|
||||
: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
onPressed: _isGroupNameEmpty
|
||||
? null
|
||||
@@ -144,7 +149,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
children: [
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
color: Colors.grey.shade50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
@@ -153,7 +160,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
child: Text(
|
||||
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -166,17 +173,14 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
itemCount: _selectedUsers.length + 1,
|
||||
separatorBuilder: (_, __) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
if (index == _selectedUsers.length) {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
);
|
||||
}
|
||||
final user = _selectedUsers[index];
|
||||
@@ -200,7 +204,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear_rounded,
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/chat_info_screen.dart';
|
||||
import 'package:example/choose_user_page.dart';
|
||||
import 'package:example/new_chat_screen.dart';
|
||||
import 'package:example/new_group_chat_screen.dart';
|
||||
import 'package:example/group_info_screen.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,7 +14,6 @@ import 'notifications_service.dart';
|
||||
import 'routes/app_routes.dart';
|
||||
import 'routes/routes.dart';
|
||||
import 'search_text_field.dart';
|
||||
import 'dart:async';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -27,7 +27,7 @@ void main() async {
|
||||
logLevel: Level.INFO,
|
||||
showLocalNotification:
|
||||
(!kIsWeb && Platform.isAndroid) ? showLocalNotification : null,
|
||||
persistenceEnabled: true,
|
||||
persistenceEnabled: false,
|
||||
);
|
||||
|
||||
if (userId != null) {
|
||||
@@ -51,18 +51,20 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamChat(
|
||||
client: client,
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
//TODO change to system once dark theme is implemented
|
||||
themeMode: ThemeMode.light,
|
||||
onGenerateRoute: AppRoutes.generateRoute,
|
||||
initialRoute:
|
||||
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
|
||||
),
|
||||
return MaterialApp(
|
||||
builder: (context, child) {
|
||||
return StreamChat(
|
||||
client: client,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
onGenerateRoute: AppRoutes.generateRoute,
|
||||
initialRoute:
|
||||
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +86,9 @@ class _HomePageState extends State<HomePage> {
|
||||
overflow: Overflow.visible,
|
||||
children: [
|
||||
StreamSvgIcon.message(
|
||||
color: _isSelected(0) ? Colors.black : Colors.grey,
|
||||
color: _isSelected(0)
|
||||
? StreamChatTheme.of(context).colorTheme.black
|
||||
: Colors.grey,
|
||||
),
|
||||
Positioned(
|
||||
top: -3,
|
||||
@@ -100,7 +104,9 @@ class _HomePageState extends State<HomePage> {
|
||||
overflow: Overflow.visible,
|
||||
children: [
|
||||
StreamSvgIcon.mentions(
|
||||
color: _isSelected(1) ? Colors.black : Colors.grey,
|
||||
color: _isSelected(1)
|
||||
? StreamChatTheme.of(context).colorTheme.black
|
||||
: Colors.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -113,6 +119,7 @@ class _HomePageState extends State<HomePage> {
|
||||
Widget build(BuildContext context) {
|
||||
final user = StreamChat.of(context).user;
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: ChannelListHeader(
|
||||
onNewChatButtonTap: () {
|
||||
Navigator.pushNamed(context, Routes.NEW_CHAT);
|
||||
@@ -121,10 +128,14 @@ class _HomePageState extends State<HomePage> {
|
||||
drawer: _buildDrawer(context, user),
|
||||
drawerEdgeDragWidth: 50,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
currentIndex: _currentIndex,
|
||||
items: _navBarItems,
|
||||
selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||
unselectedLabelStyle:
|
||||
StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: Colors.black,
|
||||
selectedItemColor: StreamChatTheme.of(context).colorTheme.black,
|
||||
unselectedItemColor: Colors.grey,
|
||||
onTap: (index) {
|
||||
setState(() => _currentIndex = index);
|
||||
@@ -142,104 +153,116 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
Drawer _buildDrawer(BuildContext context, User user) {
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).viewPadding.top + 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 20.0,
|
||||
left: 8,
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).viewPadding.top + 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 20.0,
|
||||
left: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
constraints: BoxConstraints.tight(Size.fromRadius(20)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
user.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
constraints: BoxConstraints.tight(Size.fromRadius(20)),
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.penWrite(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.NEW_CHAT,
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
'New direct message',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
user.name,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.contacts(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.NEW_GROUP_CHAT,
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
'New group',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.deleteAll();
|
||||
|
||||
StreamChat.of(context).client.disconnect(
|
||||
clearUser: true,
|
||||
);
|
||||
|
||||
await Navigator.pushReplacementNamed(
|
||||
context,
|
||||
Routes.CHOOSE_USER,
|
||||
);
|
||||
},
|
||||
leading: StreamSvgIcon.user(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
title: Text(
|
||||
'Sign out',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.penWrite(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.NEW_CHAT,
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
'New direct message',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.contacts(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.NEW_GROUP_CHAT,
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
'New group',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.deleteAll();
|
||||
|
||||
StreamChat.of(context).client.disconnect(
|
||||
clearUser: true,
|
||||
);
|
||||
|
||||
await Navigator.pushReplacementNamed(
|
||||
context,
|
||||
Routes.CHOOSE_USER,
|
||||
);
|
||||
},
|
||||
leading: StreamSvgIcon.user(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
title: Text(
|
||||
'Sign out',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -467,6 +490,32 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
||||
limit: 20,
|
||||
),
|
||||
channelWidget: ChannelPage(),
|
||||
onViewInfoTap: (channel) {
|
||||
if (channel.memberCount == 2 &&
|
||||
channel.isDistinct) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: ChatInfoScreen(
|
||||
user: channel.state.members.first.user,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: GroupInfoScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -489,7 +538,7 @@ class ChannelPageArgs {
|
||||
});
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
class ChannelPage extends StatefulWidget {
|
||||
final int initialScrollIndex;
|
||||
final double initialAlignment;
|
||||
final bool highlightInitialMessage;
|
||||
@@ -501,12 +550,75 @@ class ChannelPage extends StatelessWidget {
|
||||
this.highlightInitialMessage = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChannelPageState createState() => _ChannelPageState();
|
||||
}
|
||||
|
||||
class _ChannelPageState extends State<ChannelPage> {
|
||||
Message _quotedMessage;
|
||||
FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reply(Message message) {
|
||||
setState(() => _quotedMessage = message);
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: ChannelHeader(
|
||||
showTypingIndicator: false,
|
||||
onImageTap: () async {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
if (channel.memberCount == 2 && channel.isDistinct) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final otherUser = channel.state.members.firstWhere(
|
||||
(element) => element.user.id != currentUser.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (otherUser != null) {
|
||||
final pop = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
child: ChatInfoScreen(
|
||||
user: otherUser.user,
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (pop == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
child: GroupInfoScreen(),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
@@ -514,14 +626,35 @@ class ChannelPage extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
MessageListView(
|
||||
initialScrollIndex: initialScrollIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
highlightInitialMessage: highlightInitialMessage,
|
||||
initialScrollIndex: widget.initialScrollIndex,
|
||||
initialAlignment: widget.initialAlignment,
|
||||
highlightInitialMessage: widget.highlightInitialMessage,
|
||||
onMessageSwiped: _reply,
|
||||
onReplyTap: _reply,
|
||||
threadBuilder: (_, parentMessage) {
|
||||
return ThreadPage(
|
||||
parent: parentMessage,
|
||||
);
|
||||
},
|
||||
onShowMessage: (m, c) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
final message = m;
|
||||
final channel = client.channel(
|
||||
c.type,
|
||||
id: c.id,
|
||||
);
|
||||
if (channel.state == null) {
|
||||
await channel.watch();
|
||||
}
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
arguments: ChannelPageArgs(
|
||||
channel: channel,
|
||||
initialMessage: message,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@@ -529,7 +662,10 @@ class ChannelPage extends StatelessWidget {
|
||||
right: 0,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
color: Color(0xffFCFCFC).withOpacity(.9),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow
|
||||
.withOpacity(.9),
|
||||
child: TypingIndicator(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -542,7 +678,13 @@ class ChannelPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
MessageInput(),
|
||||
MessageInput(
|
||||
focusNode: _focusNode,
|
||||
quotedMessage: _quotedMessage,
|
||||
onQuotedMessageCleared: () {
|
||||
setState(() => _quotedMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -564,7 +706,7 @@ class ThreadPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: ThreadHeader(
|
||||
parent: parent,
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'chips_input_text_field.dart';
|
||||
import 'main.dart';
|
||||
import 'new_group_chat_screen.dart';
|
||||
import 'routes/routes.dart';
|
||||
|
||||
class NewChatScreen extends StatefulWidget {
|
||||
@@ -37,8 +36,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
|
||||
bool _showUserList = true;
|
||||
|
||||
bool _channelExisted = false;
|
||||
|
||||
void _userNameListener() {
|
||||
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||
@@ -122,17 +119,18 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
leading: const StreamBackButton(),
|
||||
title: Text(
|
||||
'New Chat',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.headlineBold
|
||||
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
@@ -157,7 +155,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 24),
|
||||
@@ -165,12 +165,19 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
|
||||
child: Text(
|
||||
user.name,
|
||||
style: TextStyle(color: Colors.black),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: .8,
|
||||
Container(
|
||||
foregroundDecoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: UserAvatar(
|
||||
showOnlineStatus: false,
|
||||
user: user,
|
||||
@@ -180,11 +187,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
child: StreamSvgIcon.close(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
StreamSvgIcon.close(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -212,7 +215,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
StreamNeumorphicButton(
|
||||
child: Center(
|
||||
child: StreamSvgIcon.contacts(
|
||||
color: Color(0xFF006CFF),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
@@ -220,10 +225,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Create a Group',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -234,15 +236,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.02),
|
||||
Colors.white.withOpacity(0.05),
|
||||
],
|
||||
stops: [0, 1],
|
||||
),
|
||||
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -250,13 +244,17 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
_isSearchActive
|
||||
? "Matches for \"$_userNameQuery\""
|
||||
: 'On the platform',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
_isSearchActive
|
||||
? "Matches for \"$_userNameQuery\""
|
||||
: 'On the platform',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5))),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -316,7 +314,17 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'No user matches these keywords...'),
|
||||
'No user matches these keywords...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(
|
||||
context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -340,7 +348,10 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
'No chats here yet...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,9 +3,8 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'group_chat_details_screen.dart';
|
||||
import 'search_text_field.dart';
|
||||
import 'routes/routes.dart';
|
||||
import 'search_text_field.dart';
|
||||
|
||||
class NewGroupChatScreen extends StatefulWidget {
|
||||
@override
|
||||
@@ -51,226 +50,229 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
forceElevated: true,
|
||||
elevation: 1,
|
||||
backgroundColor: Colors.white,
|
||||
leading: const StreamBackButton(),
|
||||
title: Text(
|
||||
'Add Group Members',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
if (_selectedUsers.isNotEmpty)
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.arrow_right(
|
||||
color: Color(0xFF006CFF),
|
||||
),
|
||||
onPressed: () async {
|
||||
final updatedList = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.NEW_GROUP_CHAT_DETAILS,
|
||||
arguments: _selectedUsers.toList(growable: false),
|
||||
);
|
||||
if (updatedList != null) {
|
||||
setState(() {
|
||||
_selectedUsers
|
||||
..clear()
|
||||
..addAll(updatedList);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
appBar: AppBar(
|
||||
elevation: 1,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
leading: const StreamBackButton(),
|
||||
title: Text(
|
||||
'Add Group Members',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
if (_selectedUsers.isNotEmpty)
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.arrow_right(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
onPressed: () async {
|
||||
final updatedList = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.NEW_GROUP_CHAT_DETAILS,
|
||||
arguments: _selectedUsers.toList(growable: false),
|
||||
);
|
||||
if (updatedList != null) {
|
||||
setState(() {
|
||||
_selectedUsers
|
||||
..clear()
|
||||
..addAll(updatedList);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: SearchTextField(
|
||||
controller: _controller,
|
||||
),
|
||||
),
|
||||
if (_selectedUsers.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: SearchTextField(
|
||||
controller: _controller,
|
||||
),
|
||||
),
|
||||
if (_selectedUsers.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 104,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _selectedUsers.length,
|
||||
padding: const EdgeInsets.all(8),
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16),
|
||||
itemBuilder: (_, index) {
|
||||
final user = _selectedUsers.elementAt(index);
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
UserAvatar(
|
||||
onlineIndicatorAlignment: Alignment(0.9, 0.9),
|
||||
user: user,
|
||||
showOnlineStatus: true,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
child: Container(
|
||||
height: 104,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _selectedUsers.length,
|
||||
padding: const EdgeInsets.all(8),
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16),
|
||||
itemBuilder: (_, index) {
|
||||
final user = _selectedUsers.elementAt(index);
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
UserAvatar(
|
||||
onlineIndicatorAlignment: Alignment(0.9, 0.9),
|
||||
user: user,
|
||||
showOnlineStatus: true,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
Positioned(
|
||||
top: -4,
|
||||
right: -4,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_selectedUsers.contains(user)) {
|
||||
setState(
|
||||
() => _selectedUsers.remove(user));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade100,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
child: StreamSvgIcon.close(
|
||||
color: Colors.black,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -4,
|
||||
right: -4,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_selectedUsers.contains(user)) {
|
||||
setState(
|
||||
() => _selectedUsers.remove(user));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
user.name.split(' ')[0],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _HeaderDelegate(
|
||||
height: 30,
|
||||
child: Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.02),
|
||||
Colors.white.withOpacity(0.05),
|
||||
],
|
||||
stops: [0, 1],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
_isSearchActive
|
||||
? 'Matches for \"$_userNameQuery\"'
|
||||
: 'On the platform',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||
child: UsersBloc(
|
||||
child: UserListView(
|
||||
selectedUsers: _selectedUsers,
|
||||
pullToRefresh: false,
|
||||
groupAlphabetically: _isSearchActive ? false : true,
|
||||
onUserTap: (user, _) {
|
||||
if (!_selectedUsers.contains(user)) {
|
||||
setState(() {
|
||||
_selectedUsers.add(user);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_selectedUsers.remove(user);
|
||||
});
|
||||
}
|
||||
},
|
||||
pagination: PaginationParams(
|
||||
limit: 25,
|
||||
),
|
||||
filter: {
|
||||
if (_userNameQuery.isNotEmpty)
|
||||
'name': {
|
||||
r'$autocomplete': _userNameQuery,
|
||||
},
|
||||
'id': {
|
||||
r'$ne': StreamChat.of(context).user.id,
|
||||
}
|
||||
},
|
||||
sort: [
|
||||
SortOption(
|
||||
'name',
|
||||
direction: 1,
|
||||
),
|
||||
],
|
||||
emptyBuilder: (_) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: StreamSvgIcon.search(
|
||||
size: 96,
|
||||
color: Colors.grey,
|
||||
child: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('No user matches these keywords...'),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
user.name.split(' ')[0],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _HeaderDelegate(
|
||||
height: 30,
|
||||
child: Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
_isSearchActive
|
||||
? 'Matches for \"$_userNameQuery\"'
|
||||
: 'On the platform',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||
child: UsersBloc(
|
||||
child: UserListView(
|
||||
selectedUsers: _selectedUsers,
|
||||
pullToRefresh: false,
|
||||
groupAlphabetically: _isSearchActive ? false : true,
|
||||
onUserTap: (user, _) {
|
||||
if (!_selectedUsers.contains(user)) {
|
||||
setState(() {
|
||||
_selectedUsers.add(user);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_selectedUsers.remove(user);
|
||||
});
|
||||
}
|
||||
},
|
||||
pagination: PaginationParams(
|
||||
limit: 25,
|
||||
),
|
||||
filter: {
|
||||
if (_userNameQuery.isNotEmpty)
|
||||
'name': {
|
||||
r'$autocomplete': _userNameQuery,
|
||||
},
|
||||
'id': {
|
||||
r'$ne': StreamChat.of(context).user.id,
|
||||
}
|
||||
},
|
||||
sort: [
|
||||
SortOption(
|
||||
'name',
|
||||
direction: 1,
|
||||
),
|
||||
],
|
||||
emptyBuilder: (_) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: StreamSvgIcon.search(
|
||||
size: 96,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'No user matches these keywords...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -292,7 +294,7 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import '../main.dart';
|
||||
import '../group_chat_details_screen.dart';
|
||||
import '../new_group_chat_screen.dart';
|
||||
import '../new_chat_screen.dart';
|
||||
import '../chat_info_screen.dart';
|
||||
import '../group_info_screen.dart';
|
||||
|
||||
class AppRoutes {
|
||||
/// Add entry for new route here
|
||||
@@ -63,6 +65,20 @@ class AppRoutes {
|
||||
selectedUsers: args,
|
||||
);
|
||||
});
|
||||
case Routes.CHAT_INFO_SCREEN:
|
||||
return MaterialPageRoute(
|
||||
settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN),
|
||||
builder: (_) {
|
||||
return ChatInfoScreen(
|
||||
user: args,
|
||||
);
|
||||
});
|
||||
case Routes.GROUP_INFO_SCREEN:
|
||||
return MaterialPageRoute(
|
||||
settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN),
|
||||
builder: (_) {
|
||||
return GroupInfoScreen();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ class Routes {
|
||||
static const String NEW_CHAT = '/new_chat';
|
||||
static const String NEW_GROUP_CHAT = '/new_group_chat';
|
||||
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details';
|
||||
static const String CHAT_INFO_SCREEN = '/chat_info_screen';
|
||||
static const String GROUP_INFO_SCREEN = '/group_info_screen';
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ class SearchTextField extends StatelessWidget {
|
||||
return Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
@@ -40,6 +40,7 @@ class SearchTextField extends StatelessWidget {
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixText: ' ',
|
||||
prefixIconConstraints: BoxConstraints.tight(Size(40, 24)),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -47,15 +48,16 @@ class SearchTextField extends StatelessWidget {
|
||||
right: 8,
|
||||
),
|
||||
child: StreamSvgIcon.search(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 14,
|
||||
),
|
||||
hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class StreamVersion extends StatelessWidget {
|
||||
const StreamVersion({
|
||||
@@ -27,8 +28,8 @@ class StreamVersion extends StatelessWidget {
|
||||
return Text(
|
||||
'Stream SDK v ${streamChatDep}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: Colors.black.withOpacity(.13),
|
||||
fontSize: 14,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: example
|
||||
description: A new Flutter project.
|
||||
version: 1.0.94+97
|
||||
version: 1.0.111+114
|
||||
|
||||
environment:
|
||||
sdk: ">=2.2.2 <3.0.0"
|
||||
@@ -22,7 +22,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
flutter_driver:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: ^0.8.0
|
||||
flutter_launcher_icons: ^0.8.1
|
||||
test: any
|
||||
|
||||
flutter:
|
||||
@@ -34,4 +34,5 @@ flutter:
|
||||
flutter_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icon.png"
|
||||
image_path_android: "assets/android_icon.png"
|
||||
image_path_ios: "assets/ios_icon.png"
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
@@ -23,12 +25,12 @@ class AttachmentError extends StatelessWidget {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
color: Color(0xffd0021B).withOpacity(.1),
|
||||
height: size?.height ?? 200,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -32,7 +32,7 @@ class AttachmentTitle extends StatelessWidget {
|
||||
attachment.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: messageTheme.messageText.copyWith(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class StreamBackButton extends StatelessWidget {
|
||||
const StreamBackButton({
|
||||
Key key,
|
||||
@@ -35,9 +37,7 @@ class StreamBackButton extends StatelessWidget {
|
||||
},
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,193 +1,223 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_info.dart';
|
||||
import 'channel_name.dart';
|
||||
import 'stream_chat.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
import 'option_list_tile.dart';
|
||||
|
||||
class ChannelBottomSheet extends StatelessWidget {
|
||||
const ChannelBottomSheet({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
class ChannelBottomSheet extends StatefulWidget {
|
||||
final VoidCallback onViewInfoTap;
|
||||
|
||||
ChannelBottomSheet({this.onViewInfoTap});
|
||||
|
||||
@override
|
||||
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
|
||||
}
|
||||
|
||||
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Center(
|
||||
child: StreamChannel(
|
||||
showLoading: false,
|
||||
channel: channel,
|
||||
child: ChannelName(
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Center(
|
||||
child: ChannelInfo(
|
||||
channel: channel,
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15.0,
|
||||
),
|
||||
child: Center(
|
||||
child: StreamBuilder<List<Member>>(
|
||||
stream: channel.state.membersStream.map((event) => event
|
||||
.where((m) => m.userId != StreamChat.of(context).user.id)
|
||||
.toList()),
|
||||
initialData: channel.state.members
|
||||
.where((m) => m.userId != StreamChat.of(context).user.id)
|
||||
.toList(),
|
||||
builder: _buildMembers,
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
if (channel.isGroup && !channel.isDistinct)
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.userRemove(
|
||||
size: 24,
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
title: Text(
|
||||
'Leave Group',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
onTap: () async {
|
||||
final confirm = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Leave Group',
|
||||
okText: 'LEAVE',
|
||||
question: 'Are you sure you want to leave this group?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.userRemove(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await channel
|
||||
.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
if ([
|
||||
'admin',
|
||||
'owner',
|
||||
].contains(channel.state.members
|
||||
.firstWhere((m) => m.userId == channel.client.state.user.id,
|
||||
orElse: () => null)
|
||||
?.role))
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
size: 24,
|
||||
),
|
||||
title: Text(
|
||||
'Delete chat',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFF3742),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question:
|
||||
'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.delete().then((value) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
var members = channel.state.members;
|
||||
|
||||
var userAsMember =
|
||||
members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id);
|
||||
var isOwner = userAsMember.role == 'owner';
|
||||
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembers(
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<Member>> snapshot,
|
||||
) {
|
||||
if (snapshot.data.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 83,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
left: (MediaQuery.of(context).size.width / 2) - 48,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: snapshot.data.map((m) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
),
|
||||
Center(
|
||||
child: ChannelInfo(
|
||||
showTypingIndicator: false,
|
||||
channel: StreamChannel.of(context).channel,
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 17.0,
|
||||
),
|
||||
if (channel.isDistinct && channel.memberCount == 2)
|
||||
Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
showOnlineStatus: true,
|
||||
user: m.user,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
constraints: BoxConstraints.tight(
|
||||
Size.square(64),
|
||||
user: members
|
||||
.firstWhere((e) => e.user.id != userAsMember.user.id)
|
||||
.user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
onlineIndicatorConstraints:
|
||||
BoxConstraints.tight(Size(16.0, 16.0)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5.0),
|
||||
child: Text(
|
||||
m.user.name?.split(' ')?.elementAt(0),
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 6.0,
|
||||
),
|
||||
Text(
|
||||
members
|
||||
.firstWhere((e) => e.user.id != userAsMember.user.id)
|
||||
.user
|
||||
.name,
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
if (!(channel.isDistinct && channel.memberCount == 2))
|
||||
Container(
|
||||
height: 94.0,
|
||||
alignment: Alignment.center,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: members.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: members[index].user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
),
|
||||
SizedBox(
|
||||
height: 6.0,
|
||||
),
|
||||
Text(
|
||||
members[index].user.name,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.user(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'View Info',
|
||||
onTap: widget.onViewInfoTap,
|
||||
),
|
||||
if (!channel.isDistinct)
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.userRemove(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'Leave Group',
|
||||
onTap: () async {
|
||||
_showLeaveDialog();
|
||||
},
|
||||
),
|
||||
if (isOwner)
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
),
|
||||
title: 'Delete Conversation',
|
||||
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
onTap: () async {
|
||||
_showDeleteDialog();
|
||||
},
|
||||
),
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.close_small(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'Cancel',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog() async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question: 'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showLeaveDialog() async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Leave conversation',
|
||||
okText: 'LEAVE',
|
||||
question: 'Are you sure you want to leave this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.userRemove(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class ChannelFileDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams paginationParams;
|
||||
|
||||
/// The builder used when the file list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
const ChannelFileDisplayScreen({
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.emptyBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
_ChannelFileDisplayScreenState createState() =>
|
||||
_ChannelFileDisplayScreenState();
|
||||
}
|
||||
|
||||
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['file'],
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
body: _buildMediaGrid(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaGrid() {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
|
||||
return StreamBuilder<List<GetMessageResponse>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.files(
|
||||
size: 136.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Text(
|
||||
'No Files',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Files sent in this chat will appear here',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final media = <Attachment, Message>{};
|
||||
|
||||
for (var item in snapshot.data) {
|
||||
item.message.attachments.where((e) => e.type == 'file').forEach((e) {
|
||||
media[e] = item.message;
|
||||
});
|
||||
}
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () => messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['file']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams.copyWith(
|
||||
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, position) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FileAttachment(
|
||||
attachment: media.keys.toList()[position],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: media.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import './channel_name.dart';
|
||||
import 'channel_image.dart';
|
||||
import 'chat_info_screen.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
/// 
|
||||
@@ -99,33 +98,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
onTap: onImageTap ??
|
||||
() async {
|
||||
if (channel.memberCount == 2 && channel.isDistinct) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final otherUser = channel.state.members.firstWhere(
|
||||
(element) => element.user.id != currentUser.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (otherUser != null) {
|
||||
final pop = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: ChatInfoScreen(
|
||||
user: otherUser.user,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (pop == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap: onImageTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -52,6 +52,9 @@ class ChannelImage extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.showOnlineStatus = true,
|
||||
this.borderRadius,
|
||||
this.selected = false,
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
@@ -67,6 +70,12 @@ class ChannelImage extends StatelessWidget {
|
||||
|
||||
final bool showOnlineStatus;
|
||||
|
||||
final bool selected;
|
||||
|
||||
final Color selectionColor;
|
||||
|
||||
final double selectionThickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamChat = StreamChat.of(context);
|
||||
@@ -94,11 +103,11 @@ class ChannelImage extends StatelessWidget {
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onTap != null
|
||||
? (_) {
|
||||
onTap();
|
||||
}
|
||||
: null,
|
||||
onTap: onTap != null ? (_) => onTap() : null,
|
||||
selected: selected,
|
||||
selectionColor: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
selectionThickness: selectionThickness,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -111,16 +120,21 @@ class ChannelImage extends StatelessWidget {
|
||||
.toList();
|
||||
return GroupImage(
|
||||
images: images,
|
||||
borderRadius: borderRadius,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onTap,
|
||||
selected: selected,
|
||||
selectionColor: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
selectionThickness: selectionThickness,
|
||||
);
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
Widget child = ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
@@ -133,7 +147,7 @@ class ChannelImage extends StatelessWidget {
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
@@ -149,7 +163,9 @@ class ChannelImage extends StatelessWidget {
|
||||
? snapshot.data['name'][0]
|
||||
: '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -169,6 +185,30 @@ class ChannelImage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selected) {
|
||||
child = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
child: Text(
|
||||
'Try Again',
|
||||
style: textStyle.copyWith(
|
||||
color: Color(0xFF006CFF),
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -99,7 +99,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color = Color(0xFF006CFF);
|
||||
color = StreamChatTheme.of(context).colorTheme.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
@@ -144,13 +144,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
Widget _buildConnectedTitleState(BuildContext context) => Text(
|
||||
'Stream Chat',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -210,7 +205,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF006CFF),
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/channels_bloc.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_bottom_sheet.dart';
|
||||
@@ -22,6 +23,8 @@ typedef ChannelTapCallback = void Function(Channel, Widget);
|
||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||
|
||||
typedef ViewInfoCallback = void Function(Channel);
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -74,6 +77,10 @@ class ChannelListView extends StatefulWidget {
|
||||
this.onStartChatPressed,
|
||||
this.swipeToAction = false,
|
||||
this.pullToRefresh = true,
|
||||
this.crossAxisCount = 1,
|
||||
this.padding,
|
||||
this.selectedChannels = const [],
|
||||
this.onViewInfoTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
@@ -134,6 +141,16 @@ class ChannelListView extends StatefulWidget {
|
||||
/// Callback used in the default empty list widget
|
||||
final VoidCallback onStartChatPressed;
|
||||
|
||||
/// The number of children in the cross axis.
|
||||
final int crossAxisCount;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
final List<Channel> selectedChannels;
|
||||
|
||||
final ViewInfoCallback onViewInfoTap;
|
||||
|
||||
@override
|
||||
_ChannelListViewState createState() => _ChannelListViewState();
|
||||
}
|
||||
@@ -141,6 +158,7 @@ class ChannelListView extends StatefulWidget {
|
||||
class _ChannelListViewState extends State<ChannelListView>
|
||||
with WidgetsBindingObserver {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final SlidableController _slideController = SlidableController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -203,16 +221,18 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.message(
|
||||
size: 136,
|
||||
color: Color(0xffDBDBDB),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Let’s start chatting!',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.headline,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -223,10 +243,14 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
child: Text(
|
||||
'How about sending your first message to a friend?',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -242,11 +266,14 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
onPressed: widget.onStartChatPressed,
|
||||
child: Text(
|
||||
'Start a chat',
|
||||
style: TextStyle(
|
||||
color:
|
||||
StreamChatTheme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -259,22 +286,35 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
}
|
||||
|
||||
if (channels.isNotEmpty) {
|
||||
child = ListView.custom(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
childrenDelegate: SliverChildBuilderDelegate(
|
||||
(context, i) {
|
||||
return _itemBuilder(context, i, channels);
|
||||
if (widget.crossAxisCount > 1) {
|
||||
child = GridView.builder(
|
||||
padding: widget.padding,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: widget.crossAxisCount),
|
||||
itemCount: channels.length,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
return _gridItemBuilder(context, index, channels);
|
||||
},
|
||||
childCount: (channels.length * 2) + 1,
|
||||
findChildIndexCallback: (key) {
|
||||
final ValueKey<String> valueKey = key;
|
||||
final index = channels.indexWhere(
|
||||
(channel) => 'CHANNEL-${channel.id}' == valueKey.value);
|
||||
return index != -1 ? (index * 2) : null;
|
||||
);
|
||||
} else {
|
||||
child = ListView.separated(
|
||||
padding: widget.padding,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
channels.isNotEmpty ? channels.length + 1 : channels.length,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
),
|
||||
);
|
||||
itemBuilder: (context, index) {
|
||||
return _listItemBuilder(context, index, channels);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,15 +328,18 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return ListView(
|
||||
padding: widget.padding,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
children: List.generate(
|
||||
25,
|
||||
(i) {
|
||||
if (i % 2 != 0) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, i);
|
||||
if (widget.crossAxisCount == 1) {
|
||||
if (i % 2 != 0) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, i);
|
||||
}
|
||||
return _separatorBuilder(context, i);
|
||||
}
|
||||
return _separatorBuilder(context, i);
|
||||
}
|
||||
return _buildLoadingItem();
|
||||
},
|
||||
@@ -305,63 +348,99 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
}
|
||||
|
||||
Shimmer _buildLoadingItem() {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Color(0xffE5E5E5),
|
||||
highlightColor: Color(0xffffffff),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
if (widget.crossAxisCount > 1) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 4.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
for (int i = 0; i < widget.crossAxisCount; i++)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 70,
|
||||
width: 70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
);
|
||||
} else {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 82,
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 238,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 42,
|
||||
width: 82,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 238,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 42,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(
|
||||
@@ -428,20 +507,10 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(context, int i, List<Channel> channels) {
|
||||
if (i % 2 != 0) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, i);
|
||||
}
|
||||
return _separatorBuilder(context, i);
|
||||
}
|
||||
|
||||
i = i ~/ 2;
|
||||
|
||||
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||
final channelsProvider = ChannelsBloc.of(context);
|
||||
if (i < channels.length) {
|
||||
final channel = channels[i];
|
||||
|
||||
ChannelTapCallback onTap;
|
||||
if (widget.onChannelTap != null) {
|
||||
onTap = widget.onChannelTap;
|
||||
@@ -493,10 +562,9 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
);
|
||||
} else {
|
||||
final backgroundColor =
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Color(0xffEBEBEB)
|
||||
: Color(0xff141414);
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke;
|
||||
child = Slidable(
|
||||
controller: _slideController,
|
||||
enabled: widget.swipeToAction,
|
||||
actionPane: SlidableBehindActionPane(),
|
||||
actionExtentRatio: 0.12,
|
||||
@@ -517,7 +585,11 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
child: ChannelBottomSheet(),
|
||||
child: ChannelBottomSheet(
|
||||
onViewInfoTap: () {
|
||||
widget.onViewInfoTap(channel);
|
||||
},
|
||||
),
|
||||
channel: channel,
|
||||
);
|
||||
},
|
||||
@@ -535,7 +607,7 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
iconWidget: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
onTap: () async {
|
||||
final res = await showConfirmationDialog(
|
||||
@@ -546,7 +618,9 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
@@ -556,7 +630,7 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).backgroundColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
child: ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
@@ -581,6 +655,47 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
}
|
||||
}
|
||||
|
||||
Widget _gridItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||
var channel = channels[i];
|
||||
|
||||
var selected = widget.selectedChannels.contains(channel);
|
||||
|
||||
return Container(
|
||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ChannelImage(
|
||||
channel: channel,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
selected: selected,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onChannelTap(channel, null);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 7),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamChannel(
|
||||
child: ChannelName(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueryProgressIndicator(
|
||||
context,
|
||||
ChannelsBlocState channelsProvider,
|
||||
@@ -591,7 +706,10 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
@@ -611,11 +729,17 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
}
|
||||
|
||||
Widget _separatorBuilder(context, i) {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
var effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
||||
|
||||
return BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: effect.sigmaX,
|
||||
sigmaY: effect.sigmaY,
|
||||
),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: effect.color.withOpacity(0.08),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class ChannelMediaDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams paginationParams;
|
||||
|
||||
/// The builder used when the file list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
const ChannelMediaDisplayScreen({
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.emptyBuilder,
|
||||
this.onShowMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
_ChannelMediaDisplayScreenState createState() =>
|
||||
_ChannelMediaDisplayScreenState();
|
||||
}
|
||||
|
||||
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['image', 'video']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Photos & Videos',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
body: _buildMediaGrid(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaGrid() {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
|
||||
return StreamBuilder<List<GetMessageResponse>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.pictures(
|
||||
size: 136.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Text(
|
||||
'No Media',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Photos or video sent in this chat will \nappear here',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final media = <_AssetPackage>[];
|
||||
|
||||
for (var item in snapshot.data) {
|
||||
item.message.attachments
|
||||
.where((e) => e.type == 'image' || e.type == 'video')
|
||||
.forEach((e) {
|
||||
VideoPlayerController controller;
|
||||
if (e.type == 'video') {
|
||||
controller = VideoPlayerController.network(e.assetUrl);
|
||||
controller.initialize();
|
||||
}
|
||||
media.add(_AssetPackage(e, item.message, controller));
|
||||
});
|
||||
}
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () => messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['image', 'video']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams.copyWith(
|
||||
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||
),
|
||||
),
|
||||
child: GridView.builder(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
|
||||
itemBuilder: (context, position) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments:
|
||||
media.map((e) => e.attachment).toList(),
|
||||
startIndex: position,
|
||||
message: media[position].message,
|
||||
sentAt: media[position].message.createdAt,
|
||||
userName: media[position].message.user.name,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: media[position].attachment.type == 'image'
|
||||
? IgnorePointer(
|
||||
child: ImageAttachment(
|
||||
attachment: media[position].attachment,
|
||||
message: media[position].message,
|
||||
showTitle: false,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
),
|
||||
)
|
||||
: VideoPlayer(media[position].videoPlayer),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: media.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetPackage {
|
||||
Attachment attachment;
|
||||
Message message;
|
||||
VideoPlayerController videoPlayer;
|
||||
|
||||
_AssetPackage(this.attachment, this.message, this.videoPlayer);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_name.dart';
|
||||
import 'channel_unread_indicator.dart';
|
||||
import 'chat_info_screen.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -50,6 +49,9 @@ class ChannelPreview extends StatelessWidget {
|
||||
return Opacity(
|
||||
opacity: snapshot.data ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap(channel);
|
||||
@@ -61,29 +63,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
leading: ChannelImage(
|
||||
onTap: onImageTap ??
|
||||
() {
|
||||
if (channel.memberCount == 2 && channel.isDistinct) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final otherUser = channel.state.members.firstWhere(
|
||||
(element) => element.user.id != currentUser.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (otherUser != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: ChatInfoScreen(
|
||||
user: otherUser.user,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap: onImageTap,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -122,12 +102,18 @@ class ChannelPreview extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: channel.state.lastMessage,
|
||||
allRead: channel.state.read
|
||||
.where((element) => element.lastRead
|
||||
size: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(channel
|
||||
.state.lastMessage.createdAt))
|
||||
.length ==
|
||||
(channel.memberCount ?? 0),
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -155,12 +141,18 @@ class ChannelPreview extends StatelessWidget {
|
||||
String stringDate;
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.year != lastMessageAt.year ||
|
||||
now.month != lastMessageAt.month ||
|
||||
now.day != lastMessageAt.day) {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
|
||||
} else {
|
||||
var startOfDay = DateTime(now.year, now.month, now.day);
|
||||
|
||||
if (lastMessageAt.millisecondsSinceEpoch >=
|
||||
startOfDay.millisecondsSinceEpoch) {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
|
||||
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
||||
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
|
||||
stringDate = 'Yesterday';
|
||||
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
||||
} else {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
|
||||
}
|
||||
|
||||
return Text(
|
||||
@@ -227,7 +219,9 @@ class ChannelPreview extends StatelessWidget {
|
||||
} else if (e.type == 'giphy') {
|
||||
return '[GIF]';
|
||||
}
|
||||
return null;
|
||||
return e == lastMessage.attachments.last
|
||||
? (e.title ?? 'File')
|
||||
: '${e.title ?? 'File'} , ';
|
||||
}).where((e) => e != null),
|
||||
lastMessage.text ?? '',
|
||||
];
|
||||
@@ -235,22 +229,69 @@ class ChannelPreview extends StatelessWidget {
|
||||
text = parts.join(' ');
|
||||
}
|
||||
|
||||
return Text(
|
||||
text,
|
||||
return Text.rich(
|
||||
_getDisplayText(
|
||||
text,
|
||||
lastMessage.mentionedUsers,
|
||||
lastMessage.attachments,
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal),
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _getDisplayText(
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle normalTextStyle,
|
||||
TextStyle mentionsTextStyle) {
|
||||
var textList = text.split(' ');
|
||||
List<TextSpan> resList = [];
|
||||
for (var e in textList) {
|
||||
if (mentions != null &&
|
||||
mentions.isNotEmpty &&
|
||||
mentions.any((element) => '@${element.name}' == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: mentionsTextStyle,
|
||||
));
|
||||
} else if (attachments != null &&
|
||||
attachments.isNotEmpty &&
|
||||
attachments
|
||||
.where((e) => e.title != null)
|
||||
.any((element) => element.title == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else {
|
||||
resList.add(TextSpan(
|
||||
text: e == textList.last ? '$e' : '$e ',
|
||||
style: normalTextStyle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(children: resList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: StreamChatTheme.of(context)
|
||||
@@ -33,7 +34,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${snapshot.data}',
|
||||
'${snapshot.data > 99 ? '99+' : snapshot.data}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
class DateDivider extends StatelessWidget {
|
||||
@@ -39,21 +40,17 @@ class DateDivider extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: (Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black
|
||||
: Colors.white)
|
||||
.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
dayInfo,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -41,8 +41,14 @@ class DeletedMessage extends StatelessWidget {
|
||||
side: borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withAlpha(24)
|
||||
: Colors.black.withAlpha(24),
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white
|
||||
.withAlpha(24)
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withAlpha(24),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
@@ -58,8 +64,8 @@ class DeletedMessage extends StatelessWidget {
|
||||
style: messageTheme.messageText.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: (Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black)
|
||||
? StreamChatTheme.of(context).colorTheme.white
|
||||
: StreamChatTheme.of(context).colorTheme.black)
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// List extension
|
||||
extension IterableX<T> on Iterable<T> {
|
||||
/// Insert any item<T> inBetween the list items
|
||||
List<T> insertBetween(T item) => expand((e) sync* {
|
||||
yield item;
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'media_utils.dart';
|
||||
|
||||
enum FileAttachmentType { local, online }
|
||||
@@ -61,13 +62,13 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
child: Container(
|
||||
width: widget.size?.width ?? 100,
|
||||
height: 56.0,
|
||||
margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius:
|
||||
widget.trailing != null ? BorderRadius.circular(16.0) : null,
|
||||
border: widget.trailing != null
|
||||
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
|
||||
? Border.fromBorderSide(BorderSide(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper))
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
@@ -88,10 +89,7 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
children: [
|
||||
Text(
|
||||
widget.attachment?.title ?? 'File',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -99,11 +97,12 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
height: 3.0,
|
||||
),
|
||||
Text(
|
||||
'${_getSizeText(widget.attachment.extraData['file_size'])}',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 14.0,
|
||||
),
|
||||
'${getSizeText(widget.attachment.extraData['file_size'])}',
|
||||
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -113,7 +112,7 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloud_download(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
launchURL(context, widget.attachment.assetUrl);
|
||||
@@ -123,37 +122,6 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ListTile(
|
||||
// dense: true,
|
||||
// leading: Container(
|
||||
// child: _getFileTypeImage(attachment.extraData['mime_type']),
|
||||
// height: 40.0,
|
||||
// width: 33.33,
|
||||
// ),
|
||||
// title: Text(
|
||||
// attachment?.title ?? 'File',
|
||||
// style: TextStyle(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// maxLines: 3,
|
||||
// ),
|
||||
// subtitle: Text(
|
||||
// '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
|
||||
// style: TextStyle(
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// ),
|
||||
// ),
|
||||
// trailing: trailing ??
|
||||
// IconButton(
|
||||
// icon: StreamSvgIcon.cloud_download(
|
||||
// color: Colors.black,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// launchURL(context, attachment.assetUrl);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -179,7 +147,8 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: StreamChatTheme.of(context).accentColor,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -226,76 +195,6 @@ class _FileAttachmentState extends State<FileAttachment> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (widget.attachment.extraData['mime_type']) {
|
||||
case '7z':
|
||||
return StreamSvgIcon.filetype_7z();
|
||||
break;
|
||||
case 'csv':
|
||||
return StreamSvgIcon.filetype_csv();
|
||||
break;
|
||||
case 'doc':
|
||||
return StreamSvgIcon.filetype_doc();
|
||||
break;
|
||||
case 'docx':
|
||||
return StreamSvgIcon.filetype_docx();
|
||||
break;
|
||||
case 'html':
|
||||
return StreamSvgIcon.filetype_html();
|
||||
break;
|
||||
case 'md':
|
||||
return StreamSvgIcon.filetype_md();
|
||||
break;
|
||||
case 'odt':
|
||||
return StreamSvgIcon.filetype_odt();
|
||||
break;
|
||||
case 'pdf':
|
||||
return StreamSvgIcon.filetype_pdf();
|
||||
break;
|
||||
case 'ppt':
|
||||
return StreamSvgIcon.filetype_ppt();
|
||||
break;
|
||||
case 'pptx':
|
||||
return StreamSvgIcon.filetype_pptx();
|
||||
break;
|
||||
case 'rar':
|
||||
return StreamSvgIcon.filetype_rar();
|
||||
break;
|
||||
case 'rtf':
|
||||
return StreamSvgIcon.filetype_rtf();
|
||||
break;
|
||||
case 'tar':
|
||||
return StreamSvgIcon.filetype_tar();
|
||||
break;
|
||||
case 'txt':
|
||||
return StreamSvgIcon.filetype_txt();
|
||||
break;
|
||||
case 'xls':
|
||||
return StreamSvgIcon.filetype_xls();
|
||||
break;
|
||||
case 'xlsx':
|
||||
return StreamSvgIcon.filetype_xlsx();
|
||||
break;
|
||||
case 'zip':
|
||||
return StreamSvgIcon.filetype_zip();
|
||||
break;
|
||||
default:
|
||||
return StreamSvgIcon.filetype_Generic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSizeText(int bytes) {
|
||||
if (bytes == null) {
|
||||
return 'Size N/A';
|
||||
}
|
||||
|
||||
if (bytes <= 1000) {
|
||||
return '${bytes} bytes';
|
||||
} else if (bytes <= 100000) {
|
||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
||||
} else {
|
||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
||||
}
|
||||
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ import 'package:photo_view/photo_view.dart';
|
||||
import 'package:stream_chat_flutter/src/image_footer.dart';
|
||||
import 'package:stream_chat_flutter/src/image_header.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
typedef ShowMessageCallback = void Function(Message message, Channel channel);
|
||||
|
||||
/// A full screen image widget
|
||||
class FullScreenMedia extends StatefulWidget {
|
||||
/// The url of the image
|
||||
@@ -18,6 +21,7 @@ class FullScreenMedia extends StatefulWidget {
|
||||
final int startIndex;
|
||||
final String userName;
|
||||
final DateTime sentAt;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
/// Instantiate a new FullScreenImage
|
||||
const FullScreenMedia({
|
||||
@@ -27,6 +31,7 @@ class FullScreenMedia extends StatefulWidget {
|
||||
this.startIndex = 0,
|
||||
this.userName = '',
|
||||
this.sentAt,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -167,6 +172,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
message: widget.message,
|
||||
urls: widget.mediaAttachments,
|
||||
currentIndex: _currentPage,
|
||||
onShowMessage: () {
|
||||
widget.onShowMessage(
|
||||
widget.message, StreamChannel.of(context).channel);
|
||||
},
|
||||
),
|
||||
ImageFooter(
|
||||
currentPage: _currentPage,
|
||||
|
||||
@@ -12,6 +12,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
const GiphyAttachment({
|
||||
Key key,
|
||||
@@ -19,6 +20,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -75,6 +77,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
}));
|
||||
@@ -112,7 +115,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
child: Material(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -124,13 +130,16 @@ class GiphyAttachment extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
'GIPHY',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
@@ -202,7 +211,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
height: 4.0,
|
||||
),
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2),
|
||||
width: double.infinity,
|
||||
height: 0.5,
|
||||
),
|
||||
@@ -220,15 +232,24 @@ class GiphyAttachment extends StatelessWidget {
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black.withOpacity(0.5)),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2),
|
||||
height: 50.0,
|
||||
),
|
||||
Expanded(
|
||||
@@ -242,7 +263,9 @@ class GiphyAttachment extends StatelessWidget {
|
||||
child: Text(
|
||||
'Send',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@@ -263,7 +286,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(
|
||||
@@ -271,10 +297,14 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -300,6 +330,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
}));
|
||||
@@ -331,7 +362,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
child: Material(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -343,13 +377,13 @@ class GiphyAttachment extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
'GIPHY',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
|
||||
@@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget {
|
||||
@required this.images,
|
||||
this.constraints,
|
||||
this.onTap,
|
||||
this.borderRadius,
|
||||
this.selected = false,
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<String> images;
|
||||
final BoxConstraints constraints;
|
||||
final VoidCallback onTap;
|
||||
final bool selected;
|
||||
final BorderRadius borderRadius;
|
||||
final Color selectionColor;
|
||||
final double selectionThickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
var avatar;
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
avatar = GestureDetector(
|
||||
onTap: onTap,
|
||||
child: ClipRRect(
|
||||
borderRadius: StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
@@ -31,7 +43,7 @@ class GroupImage extends StatelessWidget {
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
@@ -91,5 +103,25 @@ class GroupImage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
avatar = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
height: 64.0,
|
||||
width: 64.0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: avatar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class ImageActionsModal extends StatelessWidget {
|
||||
final Message message;
|
||||
final String userName;
|
||||
final String sentAt;
|
||||
final List<Attachment> urls;
|
||||
final currentIndex;
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
ImageActionsModal(
|
||||
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex});
|
||||
{this.message,
|
||||
this.userName,
|
||||
this.sentAt,
|
||||
this.urls,
|
||||
this.currentIndex,
|
||||
this.onShowMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.8),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: [0.0, 0.4],
|
||||
)),
|
||||
)),
|
||||
_buildPage(context),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,48 +38,14 @@ class ImageActionsModal extends StatelessWidget {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
width: 40.0,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
userName,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sentAt,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.copyWith(color: Colors.white.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
size: 24.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.close(),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -104,10 +58,10 @@ class ImageActionsModal extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
color: Color(0xffe5e5e5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: ListTile.divideTiles(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
context: context,
|
||||
tiles: [
|
||||
_buildButton(
|
||||
@@ -115,7 +69,8 @@ class ImageActionsModal extends StatelessWidget {
|
||||
'Reply',
|
||||
StreamSvgIcon.Icon_curve_line_left_up(
|
||||
size: 24.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() {}),
|
||||
_buildButton(
|
||||
@@ -123,17 +78,17 @@ class ImageActionsModal extends StatelessWidget {
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color: Colors.black,
|
||||
), () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onShowMessage),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.Icon_save(
|
||||
size: 24.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.grey,
|
||||
), () async {
|
||||
var url = urls[currentIndex].imageUrl ??
|
||||
urls[currentIndex].assetUrl ??
|
||||
@@ -153,7 +108,9 @@ class ImageActionsModal extends StatelessWidget {
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: Color(0xffFF3742),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
@@ -163,7 +120,9 @@ class ImageActionsModal extends StatelessWidget {
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
},
|
||||
color: Color(0xffFF3742),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
],
|
||||
).toList(),
|
||||
@@ -182,10 +141,11 @@ class ImageActionsModal extends StatelessWidget {
|
||||
{Color color}) {
|
||||
var titleStyle = TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: ListTile(
|
||||
|
||||
@@ -12,6 +12,8 @@ class ImageAttachment extends StatelessWidget {
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@@ -19,6 +21,8 @@ class ImageAttachment extends StatelessWidget {
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.messageTheme,
|
||||
this.showTitle = true,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -54,6 +58,7 @@ class ImageAttachment extends StatelessWidget {
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -83,7 +88,7 @@ class ImageAttachment extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (attachment.title != null)
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
@@ -93,7 +98,8 @@ class ImageAttachment extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
|
||||
if (showTitle &&
|
||||
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
|
||||
@@ -5,20 +5,17 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'stream_channel.dart';
|
||||
|
||||
class ImageFooter extends StatefulWidget {
|
||||
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Callback to call when pressing the back button.
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
@@ -63,12 +60,12 @@ class ImageFooter extends StatefulWidget {
|
||||
class _ImageFooterState extends State<ImageFooter> {
|
||||
bool _userSearchMode = false;
|
||||
TextEditingController _searchController;
|
||||
TextEditingController _messageController = TextEditingController();
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final FocusNode _messageFocusNode = FocusNode();
|
||||
|
||||
String _userNameQuery;
|
||||
bool _isSearchActive = false;
|
||||
String _channelNameQuery;
|
||||
|
||||
Set<User> _selectedUsers = {};
|
||||
final List<Channel> _selectedChannels = [];
|
||||
bool _loading = false;
|
||||
|
||||
Timer _debounce;
|
||||
@@ -80,8 +77,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||
if (mounted && modalSetStateCallback != null) {
|
||||
modalSetStateCallback(() {
|
||||
_userNameQuery = _searchController.text;
|
||||
_isSearchActive = _userNameQuery.isNotEmpty;
|
||||
_channelNameQuery = _searchController.text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -91,6 +87,9 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController()..addListener(_userNameListener);
|
||||
_messageFocusNode.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -103,64 +102,78 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.icon_SHARE(
|
||||
size: 24.0,
|
||||
color: Colors.black,
|
||||
return SizedBox.fromSize(
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width,
|
||||
MediaQuery.of(context).padding.bottom + widget.preferredSize.height,
|
||||
),
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: BottomAppBar(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.icon_SHARE(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
var url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
var type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
await Share.file('File', 'image.$type', bytes, 'image/$type');
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_buildShareModal(context);
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onTitleTap,
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${widget.currentPage + 1} of ${widget.totalPages}',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
InkWell(
|
||||
onTap: widget.onTitleTap,
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${widget.currentPage + 1} of ${widget.totalPages}',
|
||||
style:
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.Icon_grid(
|
||||
color: Colors.black,
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.Icon_grid(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () => _showPhotosModal(context),
|
||||
),
|
||||
onPressed: () {
|
||||
_buildPhotosModal(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhotosModal(context) {
|
||||
void _showPhotosModal(context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
@@ -170,7 +183,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
topLeft: Radius.circular(16.0),
|
||||
@@ -182,11 +195,8 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Photos',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -194,18 +204,16 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
@@ -258,292 +266,29 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareModal(context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, modalSetState) {
|
||||
modalSetStateCallback = modalSetState;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: _userSearchMode
|
||||
? 16.0
|
||||
: MediaQuery.of(context).size.height / 2,
|
||||
left: 8.0,
|
||||
right: 8.0),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Scaffold(
|
||||
body: UsersBloc(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTextInputSection(modalSetState),
|
||||
Expanded(
|
||||
child: UserListView(
|
||||
selectedUsers: _selectedUsers,
|
||||
onUserTap: (user, _) {
|
||||
_searchController.clear();
|
||||
if (!_selectedUsers.contains(user)) {
|
||||
modalSetState(() {
|
||||
_selectedUsers.add(user);
|
||||
});
|
||||
} else {
|
||||
modalSetState(() {
|
||||
_selectedUsers.remove(user);
|
||||
});
|
||||
}
|
||||
},
|
||||
crossAxisCount: 4,
|
||||
pagination: PaginationParams(
|
||||
limit: 25,
|
||||
),
|
||||
filter: {
|
||||
if (_searchController.text.isNotEmpty)
|
||||
'name': {
|
||||
r'$autocomplete': _userNameQuery,
|
||||
},
|
||||
'id': {
|
||||
r'$ne': StreamChat.of(context).user.id,
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
SortOption(
|
||||
'name',
|
||||
direction: 1,
|
||||
),
|
||||
],
|
||||
emptyBuilder: (_) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: StreamSvgIcon.search(
|
||||
size: 96,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'No user matches these keywords...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_selectedUsers.isNotEmpty)
|
||||
_buildShareTextInputSection(modalSetState),
|
||||
if (!_userSearchMode && _selectedUsers.isEmpty)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 48.0,
|
||||
child: Material(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
var url = widget
|
||||
.mediaAttachments[widget.currentPage]
|
||||
.imageUrl ??
|
||||
widget
|
||||
.mediaAttachments[widget.currentPage]
|
||||
.assetUrl ??
|
||||
widget
|
||||
.mediaAttachments[widget.currentPage]
|
||||
.thumbUrl;
|
||||
|
||||
if (widget
|
||||
.mediaAttachments[widget.currentPage]
|
||||
.type ==
|
||||
'video') {
|
||||
await _saveVideo(url);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
await _saveImage(url);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: SizedBox.expand(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Save to Photos',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextInputSection(modalSetState) {
|
||||
if (_userSearchMode) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
cursorColor: Colors.black,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIconConstraints:
|
||||
BoxConstraints.tight(Size(36.0, 44.0)),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0, horizontal: 6.0),
|
||||
child: StreamSvgIcon.search(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
hintText: 'Search',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black.withOpacity(0.08)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black.withOpacity(0.08)),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.close_small(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
onPressed: () {
|
||||
modalSetState(() {
|
||||
_userSearchMode = false;
|
||||
});
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.search(
|
||||
color: Colors.black,
|
||||
),
|
||||
iconSize: 24.0,
|
||||
onPressed: () {
|
||||
modalSetState(() {
|
||||
_userSearchMode = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Select a Chat to Share',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.share_arrow(
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
var url =
|
||||
widget.mediaAttachments[widget.currentPage].imageUrl ??
|
||||
widget.mediaAttachments[widget.currentPage].assetUrl ??
|
||||
widget.mediaAttachments[widget.currentPage].thumbUrl;
|
||||
var type =
|
||||
widget.mediaAttachments[widget.currentPage].type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes = await consolidateHttpClientResponseBytes(response);
|
||||
await Share.file('File', 'image.$type', bytes, 'image/$type');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildShareTextInputSection(modalSetState) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 56.0,
|
||||
child: _loading
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: BottomAppBar(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: _loading
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
focusNode: _messageFocusNode,
|
||||
onChanged: (val) {
|
||||
modalSetState(() {});
|
||||
},
|
||||
@@ -552,54 +297,60 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixText: ' ',
|
||||
hintText: 'Add a comment',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.16),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.16),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
)),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconTheme(
|
||||
data: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.messageInputButtonIconTheme,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
modalSetState(() {
|
||||
_loading = true;
|
||||
});
|
||||
await sendMessage();
|
||||
modalSetState(() {
|
||||
_loading = false;
|
||||
});
|
||||
},
|
||||
child: Transform.rotate(
|
||||
angle: -pi / 2,
|
||||
child: StreamSvgIcon.Icon_send_message(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
IconTheme(
|
||||
data: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.messageInputButtonIconTheme,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
modalSetState(() => _loading = true);
|
||||
await sendMessage();
|
||||
modalSetState(() => _loading = false);
|
||||
},
|
||||
splashRadius: 24,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Transform.rotate(
|
||||
angle: -pi / 2,
|
||||
child: StreamSvgIcon.Icon_send_message(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -612,27 +363,16 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
|
||||
_messageController.clear();
|
||||
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
for (var user in _selectedUsers) {
|
||||
var c = client.channel('messaging', extraData: {
|
||||
'members': [
|
||||
user.id,
|
||||
StreamChat.of(context).user.id,
|
||||
],
|
||||
});
|
||||
|
||||
await c.watch();
|
||||
|
||||
for (var channel in _selectedChannels) {
|
||||
final message = Message(
|
||||
text: text,
|
||||
attachments: [attachments[widget.currentPage]],
|
||||
);
|
||||
|
||||
await c.sendMessage(message);
|
||||
await channel.sendMessage(message);
|
||||
}
|
||||
|
||||
_selectedUsers.clear();
|
||||
_selectedChannels.clear();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:carousel_slider/carousel_options.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
@@ -12,11 +10,13 @@ class ImageGroup extends StatelessWidget {
|
||||
@required this.images,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Attachment> images;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -81,10 +81,10 @@ class ImageGroup extends StatelessWidget {
|
||||
color: Colors.black38,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${images.length - 4} +',
|
||||
'+ ${images.length - 4}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontSize: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -121,6 +121,7 @@ class ImageGroup extends StatelessWidget {
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,6 +13,9 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// Callback to call when pressing the show message button.
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
/// Callback to call when the header is tapped.
|
||||
final VoidCallback onTitleTap;
|
||||
|
||||
@@ -35,6 +38,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.currentIndex,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.onShowMessage,
|
||||
this.onTitleTap,
|
||||
this.onImageTap,
|
||||
this.userName = '',
|
||||
@@ -50,7 +54,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
onPressed: onBackPressed,
|
||||
@@ -61,7 +65,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.Icon_menu_point_v(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
_showMessageActionModalBottomSheet(context);
|
||||
@@ -81,11 +85,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
userName,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
Text(
|
||||
sentAt,
|
||||
@@ -106,6 +106,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
@@ -115,6 +116,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
message: message,
|
||||
urls: urls,
|
||||
currentIndex: currentIndex,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ class LazyLoadScrollView extends StatefulWidget {
|
||||
final bool isLoading;
|
||||
|
||||
/// Initiates a LazyLoadScrollView widget
|
||||
LazyLoadScrollView({
|
||||
const LazyLoadScrollView({
|
||||
Key key,
|
||||
@required this.child,
|
||||
this.onStartOfPage,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
extension on Duration {
|
||||
String format() {
|
||||
@@ -26,6 +29,7 @@ class MediaListView extends StatefulWidget {
|
||||
this.selectedIds = const [],
|
||||
this.onSelect,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MediaListViewState createState() => _MediaListViewState();
|
||||
}
|
||||
@@ -34,14 +38,11 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
final _media = <AssetEntity>[];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
int _currentPage = 0;
|
||||
bool _endPagination = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () {
|
||||
_getMedia();
|
||||
},
|
||||
onEndOfPage: () async => _getMedia(),
|
||||
child: GridView.builder(
|
||||
itemCount: _media.length,
|
||||
controller: _scrollController,
|
||||
@@ -80,7 +81,10 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
alignment: Alignment.topRight,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
@@ -88,10 +92,12 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.check(
|
||||
size: 24,
|
||||
color: Colors.black,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -113,7 +119,7 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
child: Text(
|
||||
media.videoDuration.format(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -141,15 +147,19 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
void _getMedia() async {
|
||||
final assetList = await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
).then((value) => value.singleWhere((element) => element.isAll));
|
||||
).then((value) {
|
||||
if (value?.isNotEmpty == true) {
|
||||
return value.singleWhere((element) => element.isAll);
|
||||
}
|
||||
});
|
||||
|
||||
if (assetList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final media = await assetList.getAssetListPaged(_currentPage, 50);
|
||||
|
||||
if (media.isEmpty) {
|
||||
setState(() {
|
||||
_endPagination = true;
|
||||
});
|
||||
} else {
|
||||
if (!media.isEmpty) {
|
||||
setState(() {
|
||||
_media.addAll(media);
|
||||
});
|
||||
|
||||
@@ -15,14 +15,17 @@ import 'stream_chat_theme.dart';
|
||||
|
||||
class MessageActionsModal extends StatelessWidget {
|
||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||
final void Function(Message) onThreadTap;
|
||||
final void Function(Message) onThreadReplyTap;
|
||||
final void Function(Message) onReplyTap;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final bool showReactions;
|
||||
final bool showDeleteMessage;
|
||||
final bool showCopyMessage;
|
||||
final bool showEditMessage;
|
||||
final bool showResendMessage;
|
||||
final bool showReply;
|
||||
final bool showThreadReply;
|
||||
final bool reverse;
|
||||
final ShapeBorder messageShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
@@ -34,9 +37,12 @@ class MessageActionsModal extends StatelessWidget {
|
||||
this.showReactions = true,
|
||||
this.showDeleteMessage = true,
|
||||
this.showEditMessage = true,
|
||||
this.onThreadTap,
|
||||
this.onReplyTap,
|
||||
this.onThreadReplyTap,
|
||||
this.showCopyMessage = true,
|
||||
this.showReply = true,
|
||||
this.showResendMessage = true,
|
||||
this.showThreadReply = true,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.editMessageInputBuilder,
|
||||
this.messageShape,
|
||||
@@ -49,27 +55,35 @@ class MessageActionsModal extends StatelessWidget {
|
||||
final user = StreamChat.of(context).user;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = message.text.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage.text.length + 40;
|
||||
if (message.quotedMessage.attachments?.isNotEmpty == true) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
messageTextLength = quotedMessageLength;
|
||||
}
|
||||
}
|
||||
final roughSentenceSize =
|
||||
message.text.length * messageTheme.messageText.fontSize * 1.2;
|
||||
messageTextLength * messageTheme.messageText.fontSize * 1.2;
|
||||
final divFactor = message.attachments?.isNotEmpty == true
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10.8731,
|
||||
sigmaY: 10.8731,
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -114,10 +128,14 @@ class MessageActionsModal extends StatelessWidget {
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showReactionPickerIndicator: true,
|
||||
showReactionPickerIndicator: showReactions &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null),
|
||||
showInChannelIndicator: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
@@ -125,9 +143,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
@@ -143,6 +159,9 @@ class MessageActionsModal extends StatelessWidget {
|
||||
horizontal: 48.0,
|
||||
),
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -159,6 +178,14 @@ class MessageActionsModal extends StatelessWidget {
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildReplyButton(context),
|
||||
if (showThreadReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (showResendMessage)
|
||||
_buildResendMessage(context),
|
||||
if (showEditMessage)
|
||||
_buildEditMessage(context),
|
||||
if (showDeleteMessage)
|
||||
@@ -182,10 +209,29 @@ class MessageActionsModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButton(BuildContext context) {
|
||||
Widget _buildReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Delete message',
|
||||
'Reply',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (onReplyTap != null) {
|
||||
onReplyTap(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButton(BuildContext context) {
|
||||
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isDeleteFailed ? 'Retry deleting message' : 'Delete message',
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red),
|
||||
),
|
||||
@@ -234,6 +280,29 @@ class MessageActionsModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResendMessage(BuildContext context) {
|
||||
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isUpdateFailed ? 'Resend edited message' : 'Resend',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.circle_up(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final client = StreamChat.of(context).client;
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (isUpdateFailed) {
|
||||
client.updateMessage(message, channel.cid);
|
||||
} else {
|
||||
channel.sendMessage(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditBottomSheet(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
showModalBottomSheet(
|
||||
@@ -241,10 +310,11 @@ class MessageActionsModal extends StatelessWidget {
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
@@ -256,39 +326,26 @@ class MessageActionsModal extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.edit(
|
||||
size: 22,
|
||||
color:
|
||||
StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.edit(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
Text(
|
||||
'Edit message',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
'Edit Message',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.cancel_outlined,
|
||||
size: 22,
|
||||
color:
|
||||
StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -301,9 +358,10 @@ class MessageActionsModal extends StatelessWidget {
|
||||
? editMessageInputBuilder(context, message)
|
||||
: MessageInput(
|
||||
editMessage: message,
|
||||
onMessageSent: (_) {
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -314,7 +372,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyButton(BuildContext context) {
|
||||
Widget _buildThreadReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Thread reply',
|
||||
@@ -325,8 +383,8 @@ class MessageActionsModal extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (onThreadTap != null) {
|
||||
onThreadTap(message);
|
||||
if (onThreadReplyTap != null) {
|
||||
onThreadReplyTap(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -21,8 +22,10 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'quoted_message_widget.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
|
||||
@@ -107,6 +110,8 @@ class MessageInput extends StatefulWidget {
|
||||
this.actionsLocation = ActionsLocation.left,
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.focusNode,
|
||||
this.quotedMessage,
|
||||
this.onQuotedMessageCleared,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
@@ -155,6 +160,12 @@ class MessageInput extends StatefulWidget {
|
||||
/// The focus node associated to the TextField
|
||||
final FocusNode focusNode;
|
||||
|
||||
///
|
||||
final Message quotedMessage;
|
||||
|
||||
///
|
||||
final VoidCallback onQuotedMessageCleared;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -166,7 +177,7 @@ class MessageInput extends StatefulWidget {
|
||||
|
||||
if (messageInputState == null) {
|
||||
throw Exception(
|
||||
'You must have a MessageInput widget as anchestor of your widget tree');
|
||||
'You must have a MessageInput widget as ancestor of your widget tree');
|
||||
}
|
||||
|
||||
return messageInputState;
|
||||
@@ -196,9 +207,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
/// The editing controller passed to the input TextField
|
||||
TextEditingController textEditingController;
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
Widget child = SafeArea(
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
if (details.delta.dy > 0) {
|
||||
@@ -213,6 +226,32 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_hasQuotedMessage)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Reply to Message',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
onPressed: widget.onQuotedMessageCleared,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildTextField(context),
|
||||
@@ -227,6 +266,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (widget.editMessage == null) {
|
||||
child = Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
elevation: 8,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
Flex _buildTextField(BuildContext context) {
|
||||
@@ -263,7 +310,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
border: _sendAsDm
|
||||
? null
|
||||
: Border.all(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
@@ -272,8 +322,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: _sendAsDm
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Colors.white,
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -288,7 +338,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: StreamSvgIcon.check(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
secondChild: SizedBox(
|
||||
height: 16,
|
||||
@@ -325,17 +375,23 @@ class MessageInputState extends State<MessageInput> {
|
||||
return AnimatedCrossFade(
|
||||
crossFadeState:
|
||||
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
firstChild: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_actionsShrunk = false;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
||||
child: StreamSvgIcon.emptyCircleLeft(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
firstChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_actionsShrunk = false;
|
||||
});
|
||||
},
|
||||
icon: StreamSvgIcon.emptyCircleLeft(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
),
|
||||
),
|
||||
secondChild: Row(
|
||||
@@ -361,13 +417,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Colors.grey,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
),
|
||||
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
@@ -408,23 +466,31 @@ class MessageInputState extends State<MessageInput> {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Chip(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).accentColor,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
padding: EdgeInsets.zero,
|
||||
labelPadding:
|
||||
EdgeInsets.symmetric(horizontal: 9.0),
|
||||
EdgeInsets.symmetric(horizontal: 8.0),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 12.0),
|
||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -432,23 +498,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
)
|
||||
: null,
|
||||
suffixIcon: _commandEnabled
|
||||
? InkWell(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: StreamSvgIcon.close_small(),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_commandEnabled = false;
|
||||
});
|
||||
? IconButton(
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
splashRadius: 24,
|
||||
onPressed: () {
|
||||
setState(() => _commandEnabled = false);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: BoxConstraints(
|
||||
maxHeight: 24.0,
|
||||
maxWidth: 40.0,
|
||||
),
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
@@ -602,12 +659,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(8.0)),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(0),
|
||||
@@ -625,13 +682,17 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
child: StreamSvgIcon.lightning(
|
||||
color: StreamChatTheme.of(context)
|
||||
.accentColor,
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Instant Commands',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -658,15 +719,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: CircleAvatar(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).accentColor,
|
||||
child: StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 12.5,
|
||||
),
|
||||
maxRadius: 12,
|
||||
),
|
||||
//subtitle: Text(c.description),
|
||||
onTap: () {
|
||||
_setCommand(c);
|
||||
@@ -693,27 +745,36 @@ class MessageInputState extends State<MessageInput> {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return _attachments.isEmpty
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: (!_attachmentContainsFile
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Colors.black.withOpacity(0.2));
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2));
|
||||
break;
|
||||
case 1:
|
||||
return _attachmentContainsFile
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: (_attachments.isEmpty
|
||||
? Colors.black.withOpacity(0.5)
|
||||
: Colors.black.withOpacity(0.2));
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2));
|
||||
break;
|
||||
case 2:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? Colors.black.withOpacity(0.2)
|
||||
: Colors.black.withOpacity(0.5);
|
||||
? StreamChatTheme.of(context).colorTheme.black.withOpacity(0.2)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5);
|
||||
break;
|
||||
case 3:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? Colors.black.withOpacity(0.2)
|
||||
: Colors.black.withOpacity(0.5);
|
||||
? StreamChatTheme.of(context).colorTheme.black.withOpacity(0.2)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -722,7 +783,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
|
||||
height: _openFilePickerSection ? _filePickerSize : 0,
|
||||
child: Material(
|
||||
color: Color(0xFFF2F2F2),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -789,7 +850,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
@@ -804,7 +865,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
width: 40.0,
|
||||
height: 4.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF2F2F2),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
),
|
||||
@@ -817,7 +879,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: _buildPickerSection(),
|
||||
@@ -852,11 +914,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: Color(0xfff2f2f2),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -885,7 +948,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: Color(0xFFF2F2F2),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -894,16 +957,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'Allow access to your gallery',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1019,7 +1086,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
CircleAvatar _buildGiphyIcon() {
|
||||
if (kIsWeb) {
|
||||
return CircleAvatar(
|
||||
backgroundColor: Colors.black,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.black,
|
||||
child: Image.asset(
|
||||
'images/giphy_icon.png',
|
||||
package: 'stream_chat_flutter',
|
||||
@@ -1080,7 +1147,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(8.0),
|
||||
elevation: 2.0,
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
@@ -1088,7 +1155,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
child: FutureBuilder<List<Member>>(
|
||||
future: queryMembers ?? Future.value(members),
|
||||
@@ -1116,7 +1183,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
subtitle: Text('@${m.userId}'),
|
||||
trailing: StreamSvgIcon.mentions(
|
||||
color: StreamChatTheme.of(context)
|
||||
.accentColor,
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
onTap: () {
|
||||
_mentionedUsers.add(m.user);
|
||||
@@ -1181,7 +1249,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(8.0),
|
||||
elevation: 2.0,
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
@@ -1196,7 +1264,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
offset: Offset(0, -4),
|
||||
),
|
||||
],
|
||||
color: StreamChatTheme.of(context).primaryColor,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(0),
|
||||
@@ -1212,14 +1280,19 @@ class MessageInputState extends State<MessageInput> {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: StreamSvgIcon.smile(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Emoji matching "$query"',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1280,6 +1353,25 @@ class MessageInputState extends State<MessageInput> {
|
||||
_commandsOverlay = null;
|
||||
}
|
||||
|
||||
Widget _buildReplyToMessage() {
|
||||
if (!_hasQuotedMessage) {
|
||||
return Offstage();
|
||||
}
|
||||
final containsUrl = widget.quotedMessage.attachments
|
||||
?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true;
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(pi),
|
||||
alignment: Alignment.center,
|
||||
child: QuotedMessageWidget(
|
||||
reverse: true,
|
||||
showBorder: !containsUrl,
|
||||
message: widget.quotedMessage,
|
||||
messageTheme: StreamChatTheme.of(context).otherMessageTheme,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachments() {
|
||||
return _attachments.isEmpty
|
||||
? Container()
|
||||
@@ -1313,10 +1405,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: InkWell(
|
||||
child: CircleAvatar(
|
||||
backgroundColor:
|
||||
Colors.black.withOpacity(0.2),
|
||||
StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.6),
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.close(
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
@@ -1401,11 +1498,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
_attachments.remove(attachment);
|
||||
});
|
||||
},
|
||||
fillColor: Colors.black.withOpacity(.5),
|
||||
fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5),
|
||||
child: Center(
|
||||
child: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1488,55 +1585,62 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _buildCommandButton() {
|
||||
return InkWell(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
|
||||
child: StreamSvgIcon.lightning(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: _commandsOverlay != null
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Color(0xFF000000).withAlpha(128),
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_animateContainer = false;
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
}
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_animateContainer = false;
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
if (_commandsOverlay == null) {
|
||||
setState(() {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
if (_commandsOverlay == null) {
|
||||
setState(() {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachmentButton() {
|
||||
var padding = widget.editMessage == null ? 4.0 : 8.0;
|
||||
return Center(
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
||||
child: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Color(0xFF000000).withAlpha(128),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
onTap: () async {
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
@@ -1854,36 +1958,29 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
sendMessage();
|
||||
},
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: Colors.grey,
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
),
|
||||
)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
sendMessage();
|
||||
},
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: IconButton(
|
||||
onPressed: sendMessage,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1922,6 +2019,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
textEditingController.clear();
|
||||
_attachments.clear();
|
||||
if (widget.onQuotedMessageCleared != null) {
|
||||
widget.onQuotedMessageCleared();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_messageIsPresent = false;
|
||||
@@ -1953,6 +2053,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.quotedMessage != null) {
|
||||
message = message.copyWith(
|
||||
quotedMessageId: widget.quotedMessage.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.preMessageSending != null) {
|
||||
message = await widget.preMessageSending(message);
|
||||
}
|
||||
@@ -1976,10 +2082,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
return sendingFuture.then((resp) {
|
||||
if (widget.onMessageSent != null) {
|
||||
widget.onMessageSent(resp.message);
|
||||
} else {
|
||||
if (widget.editMessage != null) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1995,7 +2097,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
|
||||
_emojiNames = Emoji.all().map((e) => e.name);
|
||||
@@ -2073,12 +2174,6 @@ class _SendingAttachment {
|
||||
});
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Returns the first item of the tuple
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:visibility_detector/visibility_detector.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'date_divider.dart';
|
||||
import 'stream_channel.dart';
|
||||
import 'swipeable.dart';
|
||||
|
||||
typedef MessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
@@ -29,6 +30,9 @@ typedef ParentMessageBuilder = Widget Function(
|
||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||
|
||||
typedef OnMessageSwiped = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
class MessageDetails {
|
||||
/// True if the message belongs to the current user
|
||||
bool isMyMessage;
|
||||
@@ -109,14 +113,17 @@ class MessageListView extends StatefulWidget {
|
||||
this.parentMessage,
|
||||
this.threadBuilder,
|
||||
this.onThreadTap,
|
||||
this.onReplyTap,
|
||||
this.dateDividerBuilder,
|
||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||
this.scrollPhysics = const ClampingScrollPhysics(),
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
this.scrollController,
|
||||
this.itemPositionListener,
|
||||
this.onMessageSwiped,
|
||||
this.highlightInitialMessage = false,
|
||||
this.highlightColor = const Color(0XFFFBF4DD),
|
||||
this.messageHighlightColor,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
@@ -158,15 +165,21 @@ class MessageListView extends StatefulWidget {
|
||||
/// The ScrollPhysics used by the ListView
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
/// Called when message item gets swiped
|
||||
final OnMessageSwiped onMessageSwiped;
|
||||
|
||||
///
|
||||
final ReplyTapCallback onReplyTap;
|
||||
|
||||
/// If true the list will highlight the initialMessage if there is any.
|
||||
///
|
||||
/// Also See [StreamChannel]
|
||||
final bool highlightInitialMessage;
|
||||
|
||||
/// The color used for highlighting the initialMessage if there is any.
|
||||
///
|
||||
/// Also See [highlightInitialMessage]
|
||||
final Color highlightColor;
|
||||
/// Color used while highlighting initial message
|
||||
final Color messageHighlightColor;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
@@ -179,10 +192,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bool _showScrollToBottom = false;
|
||||
ItemPositionsListener _itemPositionListener;
|
||||
int _messageListLength;
|
||||
StreamChannelState streamChannel;
|
||||
|
||||
int get _initialIndex {
|
||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
if (streamChannel.initialMessageId != null) {
|
||||
final messages = streamChannel.channel.state.messages;
|
||||
final totalMessages = messages.length;
|
||||
@@ -202,11 +215,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
bool _isInitialMessage(String id) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
return streamChannel.initialMessageId == id;
|
||||
}
|
||||
|
||||
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate;
|
||||
bool get _upToDate => streamChannel.channel.state.isUpToDate;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
@@ -222,263 +234,250 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
final messagesStream = widget.parentMessage != null
|
||||
? streamChannel.channel.state.threadsStream
|
||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||
.map((threads) => threads[widget.parentMessage.id])
|
||||
: streamChannel.channel.state?.messagesStream;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (!_upToDate) {
|
||||
await streamChannel.reloadChannel();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: StreamBuilder<List<Message>>(
|
||||
stream: messagesStream?.map((messages) => messages
|
||||
?.where((e) =>
|
||||
!e.isDeleted ||
|
||||
(e.isDeleted &&
|
||||
e.user.id == streamChannel.channel.client.state.user.id))
|
||||
?.toList()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return StreamBuilder<List<Message>>(
|
||||
stream: messagesStream?.map((messages) => messages
|
||||
?.where((e) =>
|
||||
!e.isDeleted ||
|
||||
(e.isDeleted &&
|
||||
e.user.id == streamChannel.channel.client.state.user.id))
|
||||
?.toList()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||
if (messageList.isEmpty) {
|
||||
if (_upToDate) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
messages = messageList;
|
||||
}
|
||||
|
||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||
final newMessagesListLength = messages.length;
|
||||
|
||||
if (messageList.isEmpty) {
|
||||
if (_upToDate) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
messages = messageList;
|
||||
}
|
||||
|
||||
final newMessagesListLength = messages.length;
|
||||
|
||||
if (_messageListLength != null) {
|
||||
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
|
||||
true) {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength;
|
||||
if (diff > 0) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
}
|
||||
if (_messageListLength != null) {
|
||||
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
|
||||
true) {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength;
|
||||
if (diff > 0) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
}
|
||||
} else if (!_topPaginationActive && _upToDate) {
|
||||
// Reset the index in-case we send any new message
|
||||
initialIndex = 0;
|
||||
initialAlignment = 0;
|
||||
}
|
||||
} else if (!_topPaginationActive && _upToDate) {
|
||||
// Reset the index in-case we send any new message
|
||||
initialIndex = 0;
|
||||
initialAlignment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_messageListLength = newMessagesListLength;
|
||||
_messageListLength = newMessagesListLength;
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
LazyLoadScrollView(
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
LazyLoadScrollView(
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
},
|
||||
onInBetweenOfPage: () {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
key: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
2 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 2) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
return widget.parentMessageBuilder(
|
||||
context,
|
||||
widget.parentMessage,
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0XFFF7F7F7),
|
||||
Color(0XFFFCFCFC),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (i == messages.length + 1) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
}
|
||||
if (i == 0) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
|
||||
|
||||
Widget messageWidget;
|
||||
|
||||
if (i == 1) {
|
||||
messageWidget = _buildBottomMessage(
|
||||
}
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
},
|
||||
onInBetweenOfPage: () {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
key: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
2 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 2) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
return widget.parentMessageBuilder(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else if (i == messages.length - 1) {
|
||||
messageWidget = _buildTopMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
widget.parentMessage,
|
||||
);
|
||||
} else {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (_) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
i,
|
||||
),
|
||||
messages),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (i == messages.length + 1) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
}
|
||||
if (i == 0) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
Widget messageWidget;
|
||||
|
||||
if (i == 1) {
|
||||
messageWidget = _buildBottomMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else if (i == messages.length - 1) {
|
||||
messageWidget = _buildTopMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (context) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
i,
|
||||
),
|
||||
messages),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
Positioned(
|
||||
top: 20.0,
|
||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||
valueListenable: _itemPositionListener.itemPositions,
|
||||
builder: (context, values, _) {
|
||||
final items = _itemPositionListener.itemPositions?.value;
|
||||
if (items.isEmpty || messages.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
),
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
Positioned(
|
||||
top: 20.0,
|
||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||
valueListenable: _itemPositionListener.itemPositions,
|
||||
builder: (context, values, _) {
|
||||
final items = _itemPositionListener.itemPositions?.value;
|
||||
if (items.isEmpty || messages.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var index = _getTopElement(values).index;
|
||||
var index = _getTopElement(values).index;
|
||||
|
||||
if (index > messages.length) {
|
||||
return SizedBox();
|
||||
}
|
||||
if (index > messages.length) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
if (index == messages.length) {
|
||||
index = max(index - 1, 0);
|
||||
}
|
||||
if (index == messages.length) {
|
||||
index = max(index - 1, 0);
|
||||
}
|
||||
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
messages[index].createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: messages[index].createdAt.toLocal(),
|
||||
);
|
||||
},
|
||||
),
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
messages[index].createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: messages[index].createdAt.toLocal(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _paginateData(
|
||||
@@ -498,7 +497,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
return StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel.channel.state.isUpToDateStream,
|
||||
@@ -529,9 +527,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: Colors.black,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
if (unreadCount > 0) {
|
||||
@@ -591,7 +589,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Center(
|
||||
child: Text('Error loading messages'),
|
||||
),
|
||||
@@ -696,6 +697,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return MessageWidget(
|
||||
showThreadReplyIndicator: false,
|
||||
showInChannelIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
@@ -720,6 +722,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -740,7 +743,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isNextUser =
|
||||
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final channel = streamChannel.channel;
|
||||
final readList = channel.state?.read
|
||||
?.where((element) => element.user.id != userId)
|
||||
?.where((read) =>
|
||||
@@ -753,6 +756,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
|
||||
final isThreadMessage =
|
||||
widget.parentMessage != null || message?.showInChannel == true;
|
||||
|
||||
Widget child = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
@@ -764,6 +770,26 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||
top: 3,
|
||||
),
|
||||
onQuotedMessageTap: (quotedMessageId) async {
|
||||
final scrollToIndex = () {
|
||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||
_scrollController?.scrollTo(
|
||||
index: index,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
};
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
} else {
|
||||
streamChannel.loadChannelAtMessage(quotedMessageId).then((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
showInChannelIndicator: widget.parentMessage == null,
|
||||
showThreadReplyIndicator: widget.parentMessage == null,
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
@@ -776,6 +802,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
@@ -797,21 +824,33 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
);
|
||||
|
||||
if (!isThreadMessage) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialMessageHighlightComplete &&
|
||||
widget.highlightInitialMessage &&
|
||||
_isInitialMessage(message.id)) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
final highlightColor =
|
||||
widget.messageHighlightColor ?? colorTheme.highlight;
|
||||
child = TweenAnimationBuilder<Color>(
|
||||
tween: ColorTween(
|
||||
begin: widget.highlightColor,
|
||||
end: Colors.white10,
|
||||
begin: highlightColor,
|
||||
end: colorTheme.white.withOpacity(0),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
child: child,
|
||||
onEnd: () {
|
||||
initialMessageHighlightComplete = true;
|
||||
},
|
||||
onEnd: () => initialMessageHighlightComplete = true,
|
||||
builder: (_, color, child) {
|
||||
return Container(
|
||||
color: color,
|
||||
@@ -831,7 +870,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_itemPositionListener =
|
||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
streamChannel = StreamChannel.of(context);
|
||||
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
@@ -844,7 +883,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.jumpTo(
|
||||
_scrollController?.jumpTo(
|
||||
index: 0,
|
||||
);
|
||||
});
|
||||
@@ -874,16 +913,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) {
|
||||
return StreamBuilder<Message>(
|
||||
stream: StreamChannel.of(context)
|
||||
.channel
|
||||
.state
|
||||
.messagesStream
|
||||
.map((messages) =>
|
||||
stream: streamChannel.channel.state.messagesStream.map(
|
||||
(messages) =>
|
||||
messages.firstWhere((m) => m.id == message.id)),
|
||||
initialData: message,
|
||||
builder: (_, snapshot) {
|
||||
return StreamChannel(
|
||||
channel: StreamChannel.of(context).channel,
|
||||
channel: streamChannel.channel,
|
||||
child: widget.threadBuilder(context, snapshot.data),
|
||||
);
|
||||
});
|
||||
@@ -895,6 +931,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!_upToDate) {
|
||||
streamChannel.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -40,27 +40,35 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final user = StreamChat.of(context).user;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = message.text.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage.text.length + 40;
|
||||
if (message.quotedMessage.attachments?.isNotEmpty == true) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
messageTextLength = quotedMessageLength;
|
||||
}
|
||||
}
|
||||
final roughSentenceSize =
|
||||
message.text.length * messageTheme.messageText.fontSize * 1.2;
|
||||
messageTextLength * messageTheme.messageText.fontSize * 1.2;
|
||||
final divFactor = message.attachments?.isNotEmpty == true
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10.8731,
|
||||
sigmaY: 10.8731,
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -110,14 +118,16 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
showReactionPickerIndicator: true,
|
||||
showInChannelIndicator: false,
|
||||
showReactionPickerIndicator: showReactions &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
if (message.latestReactions?.isNotEmpty == true)
|
||||
_buildReactionCard(context),
|
||||
],
|
||||
@@ -137,6 +147,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Card(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
|
||||
@@ -55,6 +55,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
||||
/// Calls [Client.search] updating [queryMessagesLoading] stream
|
||||
Future<void> search({
|
||||
Map<String, dynamic> filter,
|
||||
Map<String, dynamic> messageFilter,
|
||||
List<SortOption> sort,
|
||||
String query,
|
||||
PaginationParams pagination,
|
||||
@@ -78,6 +79,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
||||
sort,
|
||||
query,
|
||||
pagination,
|
||||
messageFilters: messageFilter,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
|
||||
@@ -113,7 +113,9 @@ class MessageSearchItem extends StatelessWidget {
|
||||
} else if (e.type == 'giphy') {
|
||||
return '[GIF]';
|
||||
}
|
||||
return null;
|
||||
return e == message.attachments.last
|
||||
? (e.title ?? 'File')
|
||||
: '${e.title ?? 'File'} , ';
|
||||
}).where((e) => e != null),
|
||||
message.text ?? '',
|
||||
];
|
||||
@@ -121,17 +123,61 @@ class MessageSearchItem extends StatelessWidget {
|
||||
text = parts.join(' ');
|
||||
}
|
||||
|
||||
return Text(
|
||||
text,
|
||||
return Text.rich(
|
||||
_getDisplayText(
|
||||
text,
|
||||
message.mentionedUsers,
|
||||
message.attachments,
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
fontStyle: (message.isSystem || message.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
fontStyle: (message.isSystem || message.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
|
||||
fontStyle: (message.isSystem || message.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _getDisplayText(
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle normalTextStyle,
|
||||
TextStyle mentionsTextStyle) {
|
||||
var textList = text.split(' ');
|
||||
List<TextSpan> resList = [];
|
||||
for (var e in textList) {
|
||||
if (mentions != null &&
|
||||
mentions.isNotEmpty &&
|
||||
mentions.any((element) => '@${element.name}' == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: mentionsTextStyle,
|
||||
));
|
||||
} else if (attachments != null &&
|
||||
attachments.isNotEmpty &&
|
||||
attachments
|
||||
.where((e) => e.title != null)
|
||||
.any((element) => element.title == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else {
|
||||
resList.add(TextSpan(
|
||||
text: e == textList.last ? '$e' : '$e ',
|
||||
style: normalTextStyle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(children: resList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/message_search_item.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'lazy_load_scroll_view.dart';
|
||||
import 'message_search_bloc.dart';
|
||||
|
||||
@@ -127,8 +128,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
? StreamChatTheme.of(context).colorTheme.white.withOpacity(0.1)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.1),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,7 +152,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
@@ -309,15 +313,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.02),
|
||||
Colors.white.withOpacity(0.05),
|
||||
],
|
||||
stops: [0, 1],
|
||||
),
|
||||
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -327,7 +323,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
child: Text(
|
||||
'${items.length} results',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -31,7 +31,7 @@ class MessageText extends StatelessWidget {
|
||||
) {
|
||||
if (link.startsWith('@')) {
|
||||
final mentionedUser = message.mentionedUsers.firstWhere(
|
||||
(u) => '@${u.name.replaceAll(' ', '')}' == link,
|
||||
(u) => '@${u.name}' == link,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
|
||||
@@ -10,14 +10,17 @@ import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
import 'image_group.dart';
|
||||
import 'message_text.dart';
|
||||
|
||||
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
|
||||
typedef OnQuotedMessageTap = void Function(String);
|
||||
|
||||
/// The display behaviour of a widget
|
||||
enum DisplayWidget {
|
||||
@@ -46,6 +49,7 @@ class MessageWidget extends StatefulWidget {
|
||||
|
||||
/// 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;
|
||||
|
||||
@@ -102,6 +106,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// If true the widget will show the thread reply indicator
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// If true the widget will show the reply indicator
|
||||
final bool showReplyIndicator;
|
||||
|
||||
/// If true the widget will show the show in channel indicator
|
||||
final bool showInChannelIndicator;
|
||||
|
||||
@@ -114,8 +121,13 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Used in [MessageReactionsModal] and [MessageActionsModal]
|
||||
final bool showReactionPickerIndicator;
|
||||
|
||||
/// If true the widget will show the resendMessage indicator
|
||||
final bool showResendMessage;
|
||||
|
||||
final List<Read> readList;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
/// If true show the users username next to the timestamp of the message
|
||||
final bool showUsername;
|
||||
final bool showTimestamp;
|
||||
@@ -126,6 +138,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Center user avatar with bottom of the message
|
||||
final bool translateUserAvatar;
|
||||
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -145,6 +160,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showSendingIndicator = DisplayWidget.show,
|
||||
this.showThreadReplyIndicator = true,
|
||||
this.showInChannelIndicator = true,
|
||||
this.showReplyIndicator = true,
|
||||
this.onReplyTap,
|
||||
this.onThreadTap,
|
||||
this.showUsername = true,
|
||||
this.showTimestamp = true,
|
||||
@@ -154,9 +171,11 @@ class MessageWidget extends StatefulWidget {
|
||||
this.onUserAvatarTap,
|
||||
this.onLinkTap,
|
||||
this.onMessageActions,
|
||||
this.onShowMessage,
|
||||
this.editMessageInputBuilder,
|
||||
this.textBuilder,
|
||||
Map<String, AttachmentBuilder> customAttachmentBuilders,
|
||||
this.showResendMessage = true,
|
||||
this.readList,
|
||||
this.padding,
|
||||
this.textPadding = const EdgeInsets.symmetric(
|
||||
@@ -165,6 +184,7 @@ class MessageWidget extends StatefulWidget {
|
||||
),
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -175,6 +195,7 @@ class MessageWidget extends StatefulWidget {
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
onShowMessage: onShowMessage,
|
||||
);
|
||||
},
|
||||
'video': (context, message, attachment) {
|
||||
@@ -186,6 +207,7 @@ class MessageWidget extends StatefulWidget {
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
);
|
||||
},
|
||||
'giphy': (context, message, attachment) {
|
||||
@@ -197,6 +219,7 @@ class MessageWidget extends StatefulWidget {
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
onShowMessage: onShowMessage,
|
||||
);
|
||||
},
|
||||
'file': (context, message, attachment) {
|
||||
@@ -224,180 +247,218 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
bool get showTimeStamp =>
|
||||
widget.message.createdAt != null && widget.showTimestamp;
|
||||
|
||||
bool get showReadList => widget.readList?.isNotEmpty == true;
|
||||
bool get isMessageRead => widget.readList?.isNotEmpty == true;
|
||||
|
||||
bool get showInChannel =>
|
||||
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
||||
|
||||
bool get hasQuotedMessage => widget.message?.quotedMessage != null;
|
||||
|
||||
bool get isSendFailed => widget.message.status == MessageSendingStatus.FAILED;
|
||||
|
||||
bool get isUpdateFailed =>
|
||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
|
||||
|
||||
bool get isDeleteFailed =>
|
||||
widget.message.status == MessageSendingStatus.FAILED_DELETE;
|
||||
|
||||
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
|
||||
|
||||
bool get isGiphy =>
|
||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||
true;
|
||||
|
||||
bool get showBottomRow =>
|
||||
showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
||||
: 6.0;
|
||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||
var leftPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 4.5;
|
||||
|
||||
final isOnlyEmoji =
|
||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
|
||||
final hasFiles =
|
||||
widget.message.attachments?.any((element) => element.type == 'file') ==
|
||||
true;
|
||||
|
||||
return Portal(
|
||||
child: Padding(
|
||||
padding: widget.padding ?? EdgeInsets.all(8),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: 0.75,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Portal(
|
||||
child: InkWell(
|
||||
onLongPress: widget.message.isDeleted && !isFailedState
|
||||
? null
|
||||
: () => onLongPress(context),
|
||||
child: Padding(
|
||||
padding: widget.padding ?? EdgeInsets.all(8),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: 0.75,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar == DisplayWidget.show)
|
||||
_buildUserAvatar(),
|
||||
SizedBox(width: 6),
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(
|
||||
width: widget.messageTheme.avatarTheme
|
||||
.constraints.maxWidth +
|
||||
8,
|
||||
),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget.message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (widget.message.isDeleted &&
|
||||
widget.message.status !=
|
||||
MessageSendingStatus
|
||||
.FAILED_DELETE)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar ==
|
||||
DisplayWidget.show) ...[
|
||||
_buildUserAvatar(),
|
||||
SizedBox(width: 4),
|
||||
],
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(width: avatarWidth + 4),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget
|
||||
.message
|
||||
.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (widget.message.isDeleted &&
|
||||
!isFailedState)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry: widget
|
||||
.borderRadiusGeometry,
|
||||
borderSide:
|
||||
widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme:
|
||||
widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: 0.0,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji &&
|
||||
!(showThreadReplyIndicator ||
|
||||
showInChannel)
|
||||
? BorderSide.none
|
||||
: widget.borderSide ??
|
||||
BorderSide(
|
||||
color: widget
|
||||
.messageTheme
|
||||
.messageBorderColor,
|
||||
),
|
||||
borderRadius: widget
|
||||
.borderRadiusGeometry ??
|
||||
BorderRadius.zero,
|
||||
),
|
||||
color: _getBackgroundColor(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
hasFiles ? 2.0 : 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (hasQuotedMessage)
|
||||
_buildQuotedMessage(),
|
||||
..._parseAttachments(
|
||||
context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(
|
||||
context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry:
|
||||
widget.borderRadiusGeometry,
|
||||
borderSide: widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme:
|
||||
widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji
|
||||
? BorderSide.none
|
||||
: widget.borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.dark
|
||||
? Colors.white
|
||||
.withAlpha(
|
||||
24)
|
||||
: Colors.black
|
||||
.withAlpha(
|
||||
24),
|
||||
),
|
||||
borderRadius: widget
|
||||
.borderRadiusGeometry ??
|
||||
BorderRadius.zero,
|
||||
),
|
||||
color: _getBackgroundColor(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
hasFiles ? 2.0 : 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
..._parseAttachments(
|
||||
context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(context),
|
||||
],
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showBottomRow) SizedBox(height: 20.0),
|
||||
],
|
||||
),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
SizedBox(height: 20.0),
|
||||
if (showBottomRow) _buildBottomRow(leftPadding),
|
||||
if (isFailedState)
|
||||
Positioned(
|
||||
left: widget.reverse ? -3 : null,
|
||||
right: widget.reverse ? null : -9,
|
||||
bottom: showBottomRow ? 20 : 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: StreamSvgIcon.error(size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
_buildBottomRow(leftPadding)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -405,6 +466,23 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuotedMessage() {
|
||||
final isMyMessage =
|
||||
widget.message.user.id == StreamChat.of(context).user.id;
|
||||
return QuotedMessageWidget(
|
||||
onTap: () {
|
||||
if (widget.onQuotedMessageTap != null) {
|
||||
widget.onQuotedMessageTap(widget.message.quotedMessageId);
|
||||
}
|
||||
},
|
||||
message: widget.message.quotedMessage,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).otherMessageTheme
|
||||
: StreamChatTheme.of(context).ownMessageTheme,
|
||||
reverse: widget.reverse,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomRow(double leftPadding) {
|
||||
final deleted = widget.message.isDeleted;
|
||||
var children = <Widget>[];
|
||||
@@ -414,16 +492,18 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -449,6 +529,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
} else {
|
||||
final showSendingIndicator =
|
||||
widget.showSendingIndicator == DisplayWidget.show;
|
||||
final threadParticipants = widget.message.threadParticipants;
|
||||
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
||||
final replyCount = widget.message.replyCount;
|
||||
final msg = replyCount != 0
|
||||
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
|
||||
@@ -461,19 +543,17 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
children.addAll([
|
||||
if (showSendingIndicator) _buildSendingIndicator(),
|
||||
if (showReadList)
|
||||
SizedBox.fromSize(
|
||||
size: Size((widget.readList.length * 10.0) + 10, 17),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: _buildReadIndicator(),
|
||||
if (showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
SizedBox.fromSize(
|
||||
size: Size((threadParticipants.length * 8.0) + 10, 16),
|
||||
child: _buildThreadParticipantsIndicator(),
|
||||
),
|
||||
),
|
||||
if (showThreadReplyIndicator)
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme?.replies),
|
||||
),
|
||||
],
|
||||
if (showUsername)
|
||||
Text(
|
||||
widget.message.user.name,
|
||||
@@ -502,7 +582,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32),
|
||||
painter: _ThreadReplyPainter(
|
||||
color: widget.messageTheme.replyThreadColor,
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -510,7 +591,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
(child) => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
child: Container(height: 16, child: Center(child: child)),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8.0)),
|
||||
@@ -526,7 +607,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
var splitList = host.split('.');
|
||||
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
_getWebsiteName(hostName.toLowerCase()) ??
|
||||
getWebsiteName(hostName.toLowerCase()) ??
|
||||
hostName.capitalize();
|
||||
|
||||
return UrlAttachment(
|
||||
@@ -536,14 +617,10 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
bool get isGiphy =>
|
||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||
true;
|
||||
|
||||
Widget _buildReadIndicator() {
|
||||
Widget _buildThreadParticipantsIndicator() {
|
||||
var padding = 0.0;
|
||||
return Stack(
|
||||
children: widget.readList.map((e) {
|
||||
children: widget.message.threadParticipants.map((user) {
|
||||
padding += 10.0;
|
||||
return Positioned(
|
||||
left: padding - 10,
|
||||
@@ -553,13 +630,10 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
color: Colors.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: UserAvatar(
|
||||
user: e.user,
|
||||
constraints: BoxConstraints.loose(Size.fromRadius(8)),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.loose(Size.fromRadius(8)),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -604,6 +678,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
@@ -618,14 +693,22 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showDeleteMessage: widget.showDeleteMessage,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onThreadTap: widget.onThreadTap,
|
||||
showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showResendMessage:
|
||||
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
||||
showCopyMessage: !isFailedState &&
|
||||
widget.message.text?.trim()?.isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
!isDeleteFailed &&
|
||||
widget.message.attachments
|
||||
?.any((element) => element.type == 'giphy') !=
|
||||
true,
|
||||
showReactions: widget.showReactions,
|
||||
showReply:
|
||||
showReply: widget.showReplyIndicator &&
|
||||
!isFailedState &&
|
||||
widget.onReplyTap != null,
|
||||
showThreadReply:
|
||||
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||
),
|
||||
);
|
||||
@@ -636,6 +719,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
@@ -663,8 +747,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
widget.borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withAlpha(24)
|
||||
: Colors.black.withAlpha(24),
|
||||
? StreamChatTheme.of(context).colorTheme.white.withAlpha(24)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withAlpha(24),
|
||||
),
|
||||
borderRadius: widget.attachmentBorderRadiusGeometry ??
|
||||
widget.borderRadiusGeometry ??
|
||||
@@ -692,6 +776,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
),
|
||||
images: images,
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -728,34 +813,20 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
}) {
|
||||
final attachmentShape =
|
||||
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
|
||||
return GestureDetector(
|
||||
onTap: () => retryMessage(context),
|
||||
onLongPress: () => onLongPress(context),
|
||||
child: Material(
|
||||
color: _getBackgroundColor(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: attachmentShape,
|
||||
child: Padding(
|
||||
padding: widget.attachmentPadding,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: attachmentShape,
|
||||
type: MaterialType.transparency,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
getFailedMessageWidget(
|
||||
context,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
attachmentWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
return Material(
|
||||
color: _getBackgroundColor(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: attachmentShape,
|
||||
child: Padding(
|
||||
padding: widget.attachmentPadding,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: attachmentShape,
|
||||
type: MaterialType.transparency,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: attachmentWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -777,89 +848,51 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
}
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
return Container(
|
||||
height: widget.messageTheme.createdAt.fontSize + 2,
|
||||
width: widget.messageTheme.createdAt.fontSize + 2,
|
||||
child: SendingIndicator(
|
||||
message: widget.message,
|
||||
allRead: widget.allRead,
|
||||
),
|
||||
final style = widget.messageTheme.createdAt;
|
||||
Widget child = SendingIndicator(
|
||||
message: widget.message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style.fontSize,
|
||||
);
|
||||
if (isMessageRead) {
|
||||
child = Row(
|
||||
children: [
|
||||
if (StreamChannel.of(context).channel.memberCount > 2)
|
||||
Text(
|
||||
widget.readList.length.toString(),
|
||||
style: style.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildUserAvatar() => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
widget.translateUserAvatar
|
||||
? widget.messageTheme.avatarTheme.constraints.maxHeight / 2
|
||||
: 0,
|
||||
),
|
||||
child: UserAvatar(
|
||||
user: widget.message.user,
|
||||
onTap: widget.onUserAvatarTap,
|
||||
constraints: widget.messageTheme.avatarTheme.constraints,
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
widget.translateUserAvatar
|
||||
? widget.messageTheme.avatarTheme.constraints.maxHeight / 2
|
||||
: 0,
|
||||
),
|
||||
child: UserAvatar(
|
||||
user: widget.message.user,
|
||||
onTap: widget.onUserAvatarTap,
|
||||
constraints: widget.messageTheme.avatarTheme.constraints,
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget getFailedMessageWidget(
|
||||
BuildContext context, {
|
||||
EdgeInsetsGeometry padding,
|
||||
}) {
|
||||
Widget failedWidget;
|
||||
if (widget.message.status == MessageSendingStatus.FAILED) {
|
||||
failedWidget = Text(
|
||||
'MESSAGE FAILED · CLICK TO TRY AGAIN',
|
||||
style: widget.messageTheme.messageText.copyWith(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
|
||||
failedWidget = Text(
|
||||
'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN',
|
||||
style: widget.messageTheme.messageText.copyWith(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
|
||||
failedWidget = Text(
|
||||
'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN',
|
||||
style: widget.messageTheme.messageText.copyWith(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (failedWidget != null) {
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: failedWidget,
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
Widget _buildTextBubble(BuildContext context) {
|
||||
Widget child = Transform(
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
@@ -867,39 +900,29 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.textPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
getFailedMessageWidget(context),
|
||||
widget.textBuilder != null
|
||||
? widget.textBuilder(context, widget.message)
|
||||
: MessageText(
|
||||
onLinkTap: widget.onLinkTap,
|
||||
message: widget.message,
|
||||
onMentionTap: widget.onMentionTap,
|
||||
messageTheme: isOnlyEmoji
|
||||
? widget.messageTheme.copyWith(
|
||||
messageText:
|
||||
widget.messageTheme.messageText.copyWith(
|
||||
fontSize: 40,
|
||||
))
|
||||
: widget.messageTheme,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: widget.textBuilder != null
|
||||
? widget.textBuilder(context, widget.message)
|
||||
: MessageText(
|
||||
onLinkTap: widget.onLinkTap,
|
||||
message: widget.message,
|
||||
onMentionTap: widget.onMentionTap,
|
||||
messageTheme: isOnlyEmoji
|
||||
? widget.messageTheme.copyWith(
|
||||
messageText:
|
||||
widget.messageTheme.messageText.copyWith(
|
||||
fontSize: 40,
|
||||
))
|
||||
: widget.messageTheme,
|
||||
),
|
||||
),
|
||||
if (widget.message.attachments
|
||||
?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true)
|
||||
?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true &&
|
||||
!hasQuotedMessage)
|
||||
_buildUrlAttachment(),
|
||||
],
|
||||
),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () => retryMessage(context),
|
||||
onLongPress: () => onLongPress(context),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isOnlyEmoji =>
|
||||
@@ -907,16 +930,14 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
if ((widget.message.status == MessageSendingStatus.FAILED ||
|
||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
|
||||
return Color(0xffd0021B).withOpacity(.1);
|
||||
if (hasQuotedMessage) {
|
||||
return widget.messageTheme.messageBackgroundColor;
|
||||
}
|
||||
|
||||
if (widget.message.attachments
|
||||
?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true) {
|
||||
return Color(0xFFE9F2FF);
|
||||
return StreamChatTheme.of(context).colorTheme.blueAlice;
|
||||
}
|
||||
|
||||
if (isOnlyEmoji) {
|
||||
@@ -952,58 +973,18 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String _getWebsiteName(String hostName) {
|
||||
switch (hostName) {
|
||||
case 'reddit':
|
||||
return 'Reddit';
|
||||
case 'youtube':
|
||||
return 'Youtube';
|
||||
case 'wikipedia':
|
||||
return 'Wikipedia';
|
||||
case 'twitter':
|
||||
return 'Twitter';
|
||||
case 'facebook':
|
||||
return 'Facebook';
|
||||
case 'amazon':
|
||||
return 'Amazon';
|
||||
case 'yelp':
|
||||
return 'Yelp';
|
||||
case 'imdb':
|
||||
return 'IMDB';
|
||||
case 'pinterest':
|
||||
return 'Pinterest';
|
||||
case 'tripadvisor':
|
||||
return 'TripAdvisor';
|
||||
case 'instagram':
|
||||
return 'Instagram';
|
||||
case 'walmart':
|
||||
return 'Walmart';
|
||||
case 'craigslist':
|
||||
return 'Craigslist';
|
||||
case 'ebay':
|
||||
return 'eBay';
|
||||
case 'linkedin':
|
||||
return 'LinkedIn';
|
||||
case 'google':
|
||||
return 'Google';
|
||||
case 'apple':
|
||||
return 'Apple';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreadReplyPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final BuildContext context;
|
||||
|
||||
const _ThreadReplyPainter({@required this.color});
|
||||
const _ThreadReplyPainter({this.context, @required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color ?? Color(0XFFDBDBDB)
|
||||
..color = color ?? StreamChatTheme.of(context).colorTheme.greyGainsboro
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
class OptionListTile extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget leading;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final Color titleColor;
|
||||
final Color tileColor;
|
||||
final Color separatorColor;
|
||||
final TextStyle titleTextStyle;
|
||||
|
||||
OptionListTile({
|
||||
this.title,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.titleColor,
|
||||
this.tileColor,
|
||||
this.separatorColor,
|
||||
this.titleTextStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: separatorColor ??
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
height: 1.0,
|
||||
),
|
||||
Material(
|
||||
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
|
||||
child: Container(
|
||||
height: 63.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) Center(child: leading),
|
||||
if (leading == null)
|
||||
SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
title,
|
||||
style: titleTextStyle ??
|
||||
(titleColor == null
|
||||
? StreamChatTheme.of(context).textTheme.bodyBold
|
||||
: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: titleColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: trailing ?? Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
import 'utils.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Attachment,
|
||||
);
|
||||
|
||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||
final Size size;
|
||||
final Attachment attachment;
|
||||
|
||||
const _VideoAttachmentThumbnail({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size = const Size(32, 32),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentThumbnailState createState() =>
|
||||
_VideoAttachmentThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl)
|
||||
..initialize().then((_) {
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.initialized
|
||||
? VideoPlayer(_controller)
|
||||
: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class QuotedMessageWidget extends StatelessWidget {
|
||||
/// The message
|
||||
final Message message;
|
||||
|
||||
/// The message theme
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
|
||||
/// If true the message will show a grey border
|
||||
final bool showBorder;
|
||||
|
||||
/// limit of the text message shown
|
||||
final int textLimit;
|
||||
|
||||
/// Map that defines a thumbnail builder for an attachment type
|
||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
attachmentThumbnailBuilders;
|
||||
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
///
|
||||
QuotedMessageWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.reverse = false,
|
||||
this.showBorder = false,
|
||||
this.textLimit = 170,
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
bool get _hasAttachments => message.attachments?.isNotEmpty == true;
|
||||
|
||||
bool get _containsScrapeUrl =>
|
||||
message.attachments?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true;
|
||||
|
||||
bool get _containsText => message?.text?.isNotEmpty == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: _buildMessage(context)),
|
||||
SizedBox(width: 4),
|
||||
_buildUserAvatar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessage(BuildContext context) {
|
||||
final isOnlyEmoji =
|
||||
message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
var msg = _hasAttachments && !_containsText
|
||||
? message.copyWith(text: message.attachments.last?.title ?? '')
|
||||
: message;
|
||||
if (msg.text.length > textLimit) {
|
||||
msg = msg.copyWith(text: '${msg.text.substring(0, textLimit - 3)}...');
|
||||
}
|
||||
|
||||
final children = [
|
||||
if (_hasAttachments) _parseAttachments(context),
|
||||
if (msg.text.isNotEmpty)
|
||||
Flexible(
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: MessageText(
|
||||
message: msg,
|
||||
messageTheme: isOnlyEmoji && _containsText
|
||||
? messageTheme.copyWith(
|
||||
messageText: messageTheme.messageText.copyWith(
|
||||
fontSize: 24,
|
||||
))
|
||||
: messageTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8));
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(context),
|
||||
border: showBorder
|
||||
? Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(12),
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: reverse ? children.reversed.toList() : children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlAttachment(Attachment attachment) {
|
||||
final size = Size(32, 32);
|
||||
if (attachment.thumbUrl != null) {
|
||||
return Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
attachment.imageUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
Widget child;
|
||||
Attachment attachment;
|
||||
if (_containsScrapeUrl) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.ogScrapeUrl != null,
|
||||
);
|
||||
child = _buildUrlAttachment(attachment);
|
||||
} else {
|
||||
QuotedMessageAttachmentThumbnailBuilder attachmentBuilder;
|
||||
attachment = message.attachments.last;
|
||||
if (attachmentThumbnailBuilders?.containsKey(attachment?.type) == true) {
|
||||
attachmentBuilder = attachmentThumbnailBuilders[attachment?.type];
|
||||
}
|
||||
attachmentBuilder = _defaultAttachmentBuilder[attachment?.type];
|
||||
if (attachmentBuilder == null) {
|
||||
child = Offstage();
|
||||
}
|
||||
child = attachmentBuilder(context, attachment);
|
||||
}
|
||||
child = AbsorbPointer(child: child);
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||
return RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserAvatar() {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: UserAvatar(
|
||||
user: message.user,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
get _defaultAttachmentBuilder {
|
||||
return {
|
||||
'image': (_, attachment) {
|
||||
return ImageAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
size: Size(32, 32),
|
||||
);
|
||||
},
|
||||
'video': (_, attachment) {
|
||||
return _VideoAttachmentThumbnail(
|
||||
key: ValueKey(attachment.assetUrl),
|
||||
attachment: attachment,
|
||||
);
|
||||
},
|
||||
'giphy': (_, attachment) {
|
||||
final size = Size(32, 32);
|
||||
return CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl:
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
'file': (_, attachment) {
|
||||
return Container(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Color _getBackgroundColor(BuildContext context) {
|
||||
if (_containsScrapeUrl) {
|
||||
return StreamChatTheme.of(context).colorTheme.blueAlice;
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -107,16 +107,22 @@ class ReactionBubble extends StatelessWidget {
|
||||
height: 16,
|
||||
color: (!highlightOwnReactions ||
|
||||
reaction.user.id == StreamChat.of(context).user.id)
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Colors.black.withOpacity(.5),
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
)
|
||||
: Icon(
|
||||
Icons.help_outline_rounded,
|
||||
size: 16,
|
||||
color: (!highlightOwnReactions ||
|
||||
reaction.user.id == StreamChat.of(context).user.id)
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Colors.black.withOpacity(.5),
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
height: animations[index].value * 24.0,
|
||||
width: animations[index].value * 24.0,
|
||||
color: ownReactionIndex != -1
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue
|
||||
: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
@@ -134,11 +136,11 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
|
||||
/// Add a reaction to the message
|
||||
void sendReaction(BuildContext context, String reactionType) {
|
||||
StreamChannel.of(context)
|
||||
.channel
|
||||
.sendReaction(widget.message, reactionType, extraData: {
|
||||
'enforce_unique': true,
|
||||
});
|
||||
StreamChannel.of(context).channel.sendReaction(
|
||||
widget.message,
|
||||
reactionType,
|
||||
enforceUnique: true,
|
||||
);
|
||||
pop();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,27 +4,27 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// Used to show the sending status of the message
|
||||
class SendingIndicator extends StatelessWidget {
|
||||
final Message message;
|
||||
final bool allRead;
|
||||
final bool isMessageRead;
|
||||
final double size;
|
||||
|
||||
const SendingIndicator({
|
||||
Key key,
|
||||
this.message,
|
||||
this.allRead = false,
|
||||
this.isMessageRead = false,
|
||||
this.size = 12,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (allRead) {
|
||||
return Icon(
|
||||
Icons.done_all,
|
||||
size: 8,
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
if (isMessageRead) {
|
||||
return StreamSvgIcon.checkAll(
|
||||
size: size,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
);
|
||||
}
|
||||
if (message.status == MessageSendingStatus.SENT || message.status == null) {
|
||||
return Icon(
|
||||
Icons.done,
|
||||
size: 8,
|
||||
return StreamSvgIcon.check(
|
||||
size: size,
|
||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
@@ -32,18 +32,9 @@ class SendingIndicator extends StatelessWidget {
|
||||
message.status == MessageSendingStatus.UPDATING) {
|
||||
return Icon(
|
||||
Icons.access_time,
|
||||
size: 8,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
if (message.status == MessageSendingStatus.FAILED ||
|
||||
message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||||
message.status == MessageSendingStatus.FAILED_DELETE) {
|
||||
return Icon(
|
||||
Icons.error_outline,
|
||||
size: 8,
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
||||
return Theme(
|
||||
data: materialTheme.copyWith(
|
||||
primaryIconTheme: streamTheme.primaryIconTheme,
|
||||
accentColor: streamTheme.accentColor,
|
||||
scaffoldBackgroundColor: streamTheme.backgroundColor,
|
||||
accentColor: streamTheme.colorTheme.accentBlue,
|
||||
scaffoldBackgroundColor: streamTheme.colorTheme.white,
|
||||
buttonTheme: streamTheme.buttonTheme,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
@@ -91,81 +92,7 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
||||
StreamChatThemeData themeData,
|
||||
) {
|
||||
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
|
||||
final theme = defaultTheme.copyWith(
|
||||
primaryColor: themeData?.primaryColor,
|
||||
defaultChannelImage: themeData?.defaultChannelImage,
|
||||
primaryIconTheme: themeData?.primaryIconTheme,
|
||||
defaultUserImage: themeData?.defaultUserImage,
|
||||
backgroundColor: themeData?.backgroundColor,
|
||||
channelTheme: defaultTheme.channelTheme.copyWith(
|
||||
channelHeaderTheme:
|
||||
defaultTheme.channelTheme.channelHeaderTheme.copyWith(
|
||||
color: themeData?.channelTheme?.channelHeaderTheme?.color,
|
||||
lastMessageAt:
|
||||
themeData?.channelTheme?.channelHeaderTheme?.lastMessageAt,
|
||||
title: themeData?.channelTheme?.channelHeaderTheme?.title,
|
||||
avatarTheme: defaultTheme.channelPreviewTheme.avatarTheme.copyWith(
|
||||
constraints: themeData
|
||||
?.channelTheme?.channelHeaderTheme?.avatarTheme?.constraints,
|
||||
borderRadius: themeData
|
||||
?.channelTheme?.channelHeaderTheme?.avatarTheme?.borderRadius,
|
||||
),
|
||||
),
|
||||
inputBackground: themeData?.channelTheme?.inputBackground,
|
||||
messageInputButtonIconTheme:
|
||||
themeData?.channelTheme?.messageInputButtonIconTheme,
|
||||
inputGradient: themeData?.channelTheme?.inputGradient,
|
||||
messageInputButtonTheme:
|
||||
themeData?.channelTheme?.messageInputButtonTheme,
|
||||
),
|
||||
ownMessageTheme: defaultTheme.ownMessageTheme.copyWith(
|
||||
replies: themeData?.ownMessageTheme?.replies,
|
||||
createdAt: themeData?.ownMessageTheme?.createdAt,
|
||||
messageText: themeData?.ownMessageTheme?.messageText,
|
||||
messageBackgroundColor:
|
||||
themeData?.ownMessageTheme?.messageBackgroundColor,
|
||||
messageAuthor: themeData?.ownMessageTheme?.messageAuthor,
|
||||
messageLinks: themeData?.ownMessageTheme?.messageLinks,
|
||||
avatarTheme: defaultTheme.ownMessageTheme.avatarTheme.copyWith(
|
||||
constraints: themeData?.ownMessageTheme?.avatarTheme?.constraints,
|
||||
borderRadius: themeData?.ownMessageTheme?.avatarTheme?.borderRadius,
|
||||
),
|
||||
reactionsBorderColor: themeData?.ownMessageTheme?.reactionsBorderColor,
|
||||
reactionsBackgroundColor:
|
||||
themeData?.ownMessageTheme?.reactionsBackgroundColor,
|
||||
),
|
||||
otherMessageTheme: defaultTheme.otherMessageTheme.copyWith(
|
||||
replies: themeData?.otherMessageTheme?.replies,
|
||||
createdAt: themeData?.otherMessageTheme?.createdAt,
|
||||
messageText: themeData?.otherMessageTheme?.messageText,
|
||||
messageBackgroundColor:
|
||||
themeData?.otherMessageTheme?.messageBackgroundColor,
|
||||
messageAuthor: themeData?.otherMessageTheme?.messageAuthor,
|
||||
messageLinks: themeData?.otherMessageTheme?.messageLinks,
|
||||
avatarTheme: defaultTheme.otherMessageTheme.avatarTheme.copyWith(
|
||||
constraints: themeData?.otherMessageTheme?.avatarTheme?.constraints,
|
||||
borderRadius: themeData?.otherMessageTheme?.avatarTheme?.borderRadius,
|
||||
),
|
||||
reactionsBorderColor:
|
||||
themeData?.otherMessageTheme?.reactionsBorderColor,
|
||||
reactionsBackgroundColor:
|
||||
themeData?.otherMessageTheme?.reactionsBackgroundColor,
|
||||
),
|
||||
accentColor: themeData?.accentColor,
|
||||
secondaryColor: themeData?.secondaryColor,
|
||||
channelPreviewTheme: defaultTheme.channelPreviewTheme.copyWith(
|
||||
avatarTheme: defaultTheme.channelPreviewTheme.avatarTheme.copyWith(
|
||||
constraints: themeData?.channelPreviewTheme?.avatarTheme?.constraints,
|
||||
borderRadius:
|
||||
themeData?.channelPreviewTheme?.avatarTheme?.borderRadius,
|
||||
),
|
||||
title: themeData?.channelPreviewTheme?.title,
|
||||
lastMessageAt: themeData?.channelPreviewTheme?.lastMessageAt,
|
||||
subtitle: themeData?.channelPreviewTheme?.subtitle,
|
||||
unreadCounterColor: themeData?.channelPreviewTheme?.unreadCounterColor,
|
||||
),
|
||||
);
|
||||
return theme;
|
||||
return defaultTheme.merge(themeData) ?? themeData;
|
||||
}
|
||||
|
||||
/// The current user
|
||||
|
||||
@@ -42,17 +42,14 @@ class StreamChatTheme extends InheritedWidget {
|
||||
|
||||
/// Theme data
|
||||
class StreamChatThemeData {
|
||||
/// Primary color of the chat widgets
|
||||
final Color primaryColor;
|
||||
/// The text themes used in the widgets
|
||||
final TextTheme textTheme;
|
||||
|
||||
/// Secondary color of the chat widgets
|
||||
final Color secondaryColor;
|
||||
/// The button themes used in the widgets
|
||||
final ButtonThemeData buttonTheme;
|
||||
|
||||
/// Accent color of the chat widgets
|
||||
final Color accentColor;
|
||||
|
||||
/// Background color of the chat widgets
|
||||
final Color backgroundColor;
|
||||
/// The text themes used in the widgets
|
||||
final ColorTheme colorTheme;
|
||||
|
||||
/// Theme of the [ChannelPreview]
|
||||
final ChannelPreviewTheme channelPreviewTheme;
|
||||
@@ -79,11 +76,10 @@ class StreamChatThemeData {
|
||||
final List<ReactionIcon> reactionIcons;
|
||||
|
||||
/// Create a theme from scratch
|
||||
StreamChatThemeData({
|
||||
this.primaryColor,
|
||||
this.secondaryColor,
|
||||
this.accentColor,
|
||||
this.backgroundColor,
|
||||
const StreamChatThemeData({
|
||||
this.textTheme,
|
||||
this.buttonTheme,
|
||||
this.colorTheme,
|
||||
this.channelPreviewTheme,
|
||||
this.channelTheme,
|
||||
this.otherMessageTheme,
|
||||
@@ -97,44 +93,25 @@ class StreamChatThemeData {
|
||||
/// Create a theme from a Material [Theme]
|
||||
factory StreamChatThemeData.fromTheme(ThemeData theme) {
|
||||
final defaultTheme = getDefaultTheme(theme);
|
||||
|
||||
return defaultTheme.copyWith(
|
||||
accentColor: theme.accentColor,
|
||||
final customizedTheme = StreamChatThemeData(
|
||||
primaryIconTheme: theme.primaryIconTheme,
|
||||
primaryColor: theme.colorScheme.primary,
|
||||
secondaryColor: theme.colorScheme.secondary,
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
channelTheme: defaultTheme.channelTheme.copyWith(
|
||||
inputGradient: LinearGradient(colors: [
|
||||
theme.accentColor.withOpacity(.5),
|
||||
theme.accentColor,
|
||||
]),
|
||||
ownMessageTheme: MessageTheme(
|
||||
replies: TextStyle(color: theme.accentColor),
|
||||
messageLinks: TextStyle(color: theme.accentColor),
|
||||
),
|
||||
ownMessageTheme: defaultTheme.ownMessageTheme.copyWith(
|
||||
replies: defaultTheme.ownMessageTheme.replies.copyWith(
|
||||
color: theme.accentColor,
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: theme.accentColor,
|
||||
),
|
||||
),
|
||||
otherMessageTheme: defaultTheme.otherMessageTheme.copyWith(
|
||||
replies: defaultTheme.otherMessageTheme.replies.copyWith(
|
||||
color: theme.accentColor,
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: theme.accentColor,
|
||||
),
|
||||
otherMessageTheme: MessageTheme(
|
||||
replies: TextStyle(color: theme.accentColor),
|
||||
messageLinks: TextStyle(color: theme.accentColor),
|
||||
),
|
||||
);
|
||||
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
|
||||
}
|
||||
|
||||
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
|
||||
StreamChatThemeData copyWith({
|
||||
Color primaryColor,
|
||||
Color secondaryColor,
|
||||
Color accentColor,
|
||||
Color backgroundColor,
|
||||
TextTheme textTheme,
|
||||
ButtonThemeData buttonTheme,
|
||||
ColorTheme colorTheme,
|
||||
ChannelPreviewTheme channelPreviewTheme,
|
||||
ChannelTheme channelTheme,
|
||||
MessageTheme ownMessageTheme,
|
||||
@@ -145,91 +122,63 @@ class StreamChatThemeData {
|
||||
List<ReactionIcon> reactionIcons,
|
||||
}) =>
|
||||
StreamChatThemeData(
|
||||
primaryColor: primaryColor ?? this.primaryColor,
|
||||
secondaryColor: secondaryColor ?? this.secondaryColor,
|
||||
textTheme: textTheme ?? this.textTheme,
|
||||
buttonTheme: buttonTheme ?? this.buttonTheme,
|
||||
colorTheme: colorTheme ?? this.colorTheme,
|
||||
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||
accentColor: accentColor ?? this.accentColor,
|
||||
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
|
||||
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
channelPreviewTheme: channelPreviewTheme?.copyWith(
|
||||
title:
|
||||
channelPreviewTheme.title ?? this.channelPreviewTheme.title,
|
||||
subtitle: channelPreviewTheme.subtitle ??
|
||||
this.channelPreviewTheme.subtitle,
|
||||
lastMessageAt: channelPreviewTheme.lastMessageAt ??
|
||||
this.channelPreviewTheme.lastMessageAt,
|
||||
avatarTheme: channelPreviewTheme.avatarTheme ??
|
||||
this.channelPreviewTheme.avatarTheme,
|
||||
) ??
|
||||
this.channelPreviewTheme,
|
||||
channelTheme: channelTheme?.copyWith(
|
||||
channelHeaderTheme: channelTheme.channelHeaderTheme ??
|
||||
this.channelTheme.channelHeaderTheme,
|
||||
messageInputButtonIconTheme:
|
||||
channelTheme.messageInputButtonIconTheme ??
|
||||
this.channelTheme.messageInputButtonIconTheme,
|
||||
messageInputButtonTheme: channelTheme.messageInputButtonTheme ??
|
||||
this.channelTheme.messageInputButtonTheme,
|
||||
inputGradient:
|
||||
channelTheme.inputGradient ?? this.channelTheme.inputGradient,
|
||||
inputBackground: channelTheme.inputBackground ??
|
||||
this.channelTheme.inputBackground,
|
||||
) ??
|
||||
this.channelTheme,
|
||||
ownMessageTheme: ownMessageTheme?.copyWith(
|
||||
messageText: ownMessageTheme?.messageText ??
|
||||
this.ownMessageTheme.messageText,
|
||||
messageAuthor: ownMessageTheme?.messageAuthor ??
|
||||
this.ownMessageTheme.messageAuthor,
|
||||
messageLinks: ownMessageTheme?.messageLinks ??
|
||||
this.ownMessageTheme.messageLinks,
|
||||
createdAt:
|
||||
ownMessageTheme?.createdAt ?? this.ownMessageTheme.createdAt,
|
||||
replies: ownMessageTheme?.replies ?? this.ownMessageTheme.replies,
|
||||
messageBackgroundColor: ownMessageTheme?.messageBackgroundColor ??
|
||||
this.ownMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: ownMessageTheme?.avatarTheme ??
|
||||
this.ownMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme?.copyWith(
|
||||
messageText: otherMessageTheme?.messageText ??
|
||||
this.otherMessageTheme.messageText,
|
||||
messageAuthor: otherMessageTheme?.messageAuthor ??
|
||||
this.otherMessageTheme.messageAuthor,
|
||||
messageLinks: otherMessageTheme?.messageLinks ??
|
||||
this.otherMessageTheme.messageLinks,
|
||||
createdAt: otherMessageTheme?.createdAt ??
|
||||
this.otherMessageTheme.createdAt,
|
||||
replies:
|
||||
otherMessageTheme?.replies ?? this.otherMessageTheme.replies,
|
||||
messageBackgroundColor:
|
||||
otherMessageTheme?.messageBackgroundColor ??
|
||||
this.otherMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: otherMessageTheme?.avatarTheme ??
|
||||
this.otherMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.otherMessageTheme,
|
||||
channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
|
||||
channelTheme: channelTheme ?? this.channelTheme,
|
||||
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
|
||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||
);
|
||||
|
||||
StreamChatThemeData merge(StreamChatThemeData other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
|
||||
buttonTheme: other.buttonTheme,
|
||||
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
|
||||
primaryIconTheme: other.primaryIconTheme,
|
||||
defaultChannelImage: other.defaultChannelImage,
|
||||
defaultUserImage: other.defaultUserImage,
|
||||
channelPreviewTheme:
|
||||
channelPreviewTheme?.merge(other.channelPreviewTheme) ??
|
||||
other.channelPreviewTheme,
|
||||
channelTheme:
|
||||
channelTheme?.merge(other.channelTheme) ?? other.channelTheme,
|
||||
ownMessageTheme: ownMessageTheme?.merge(other.ownMessageTheme) ??
|
||||
other.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
|
||||
other.otherMessageTheme,
|
||||
reactionIcons: other.reactionIcons,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the default Stream Chat theme
|
||||
static StreamChatThemeData getDefaultTheme(ThemeData theme) {
|
||||
final accentColor = Color(0xff006cff);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
|
||||
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
|
||||
return StreamChatThemeData(
|
||||
secondaryColor: Color(0xffEAEAEA),
|
||||
accentColor: accentColor,
|
||||
primaryColor: isDark ? Colors.black : Colors.white,
|
||||
primaryIconTheme: IconThemeData(
|
||||
color: isDark ? Colors.white : Colors.black.withOpacity(.5)),
|
||||
textTheme: textTheme,
|
||||
colorTheme: colorTheme,
|
||||
buttonTheme: ButtonThemeData(
|
||||
height: 48.0,
|
||||
buttonColor: isDark ? Color(0xffffffff) : Color(0xff006aff),
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
colorScheme: theme.colorScheme.copyWith(
|
||||
secondary: isDark ? Color(0xff005eff) : Color(0xffffffff),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
|
||||
defaultChannelImage: (context, channel) => SizedBox(),
|
||||
backgroundColor: isDark ? Colors.black : Colors.white,
|
||||
defaultUserImage: (context, user) => Center(
|
||||
child: CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
@@ -238,29 +187,22 @@ class StreamChatThemeData {
|
||||
),
|
||||
),
|
||||
channelPreviewTheme: ChannelPreviewTheme(
|
||||
unreadCounterColor: Color(0xffff3742),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
unreadCounterColor: colorTheme.accentRed,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
fontWeight: FontWeight.bold),
|
||||
subtitle: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: (isDark ? Colors.white : Colors.black).withOpacity(0.5),
|
||||
),
|
||||
lastMessageAt: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
title: textTheme.bodyBold,
|
||||
subtitle: textTheme.footnote.copyWith(
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
lastMessageAt: textTheme.footnote.copyWith(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
),
|
||||
indicatorIconSize: 16.0),
|
||||
channelTheme: ChannelTheme(
|
||||
messageInputButtonIconTheme: theme.iconTheme.copyWith(
|
||||
color: accentColor,
|
||||
@@ -273,34 +215,25 @@ class StreamChatThemeData {
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
color: isDark ? Colors.black : Colors.white,
|
||||
color: colorTheme.white,
|
||||
title: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
lastMessageAt: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
inputBackground:
|
||||
isDark ? Colors.black.withAlpha(12) : Colors.white.withAlpha(12),
|
||||
inputGradient: LinearGradient(colors: [
|
||||
Color(0xFF00AEFF),
|
||||
Color(0xFF0076FF),
|
||||
]),
|
||||
inputBackground: colorTheme.white.withAlpha(12),
|
||||
),
|
||||
ownMessageTheme: MessageTheme(
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
@@ -308,10 +241,10 @@ class StreamChatThemeData {
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
messageBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBackgroundColor: colorTheme.white,
|
||||
reactionsBorderColor: colorTheme.greyWhisper,
|
||||
messageBorderColor: colorTheme.greyGainsboro,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -324,17 +257,14 @@ class StreamChatThemeData {
|
||||
),
|
||||
),
|
||||
otherMessageTheme: MessageTheme(
|
||||
reactionsBackgroundColor:
|
||||
isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
reactionsBorderColor: isDark ? Colors.black : Colors.white,
|
||||
reactionsBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBorderColor: colorTheme.white,
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
@@ -345,9 +275,8 @@ class StreamChatThemeData {
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
replyThreadColor:
|
||||
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
|
||||
messageBackgroundColor: colorTheme.white,
|
||||
messageBorderColor: colorTheme.greyWhisper,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -362,19 +291,19 @@ class StreamChatThemeData {
|
||||
assetName: 'Icon_love_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'thumbs_up',
|
||||
type: 'like',
|
||||
assetName: 'Icon_thumbs_up_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'thumbs_down',
|
||||
type: 'sad',
|
||||
assetName: 'Icon_thumbs_down_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'lol',
|
||||
type: 'haha',
|
||||
assetName: 'Icon_LOL_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'wut',
|
||||
type: 'wow',
|
||||
assetName: 'Icon_wut_reaction.svg',
|
||||
),
|
||||
],
|
||||
@@ -382,6 +311,337 @@ class StreamChatThemeData {
|
||||
}
|
||||
}
|
||||
|
||||
enum TextThemeType {
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
class TextTheme {
|
||||
final TextStyle title;
|
||||
final TextStyle headlineBold;
|
||||
final TextStyle headline;
|
||||
final TextStyle bodyBold;
|
||||
final TextStyle body;
|
||||
final TextStyle footnoteBold;
|
||||
final TextStyle footnote;
|
||||
final TextStyle captionBold;
|
||||
|
||||
TextTheme.light({
|
||||
this.title = const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.headlineBold = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.headline = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.bodyBold = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.body = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.footnoteBold = const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.footnote = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.captionBold = const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
});
|
||||
|
||||
TextTheme.dark({
|
||||
this.title = const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.headlineBold = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.headline = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.bodyBold = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.body = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.footnoteBold = const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.footnote = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.captionBold = const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
});
|
||||
|
||||
TextTheme copyWith({
|
||||
TextThemeType type = TextThemeType.light,
|
||||
TextStyle body,
|
||||
TextStyle title,
|
||||
TextStyle headlineBold,
|
||||
TextStyle headline,
|
||||
TextStyle bodyBold,
|
||||
TextStyle footnoteBold,
|
||||
TextStyle footnote,
|
||||
TextStyle captionBold,
|
||||
}) {
|
||||
return type == TextThemeType.light
|
||||
? TextTheme.light(
|
||||
body: body ?? this.body,
|
||||
title: title ?? this.title,
|
||||
headlineBold: headlineBold ?? this.headlineBold,
|
||||
headline: headline ?? this.headline,
|
||||
bodyBold: bodyBold ?? this.bodyBold,
|
||||
footnoteBold: footnoteBold ?? this.footnoteBold,
|
||||
footnote: footnote ?? this.footnote,
|
||||
captionBold: captionBold ?? this.captionBold,
|
||||
)
|
||||
: TextTheme.dark(
|
||||
body: body ?? this.body,
|
||||
title: title ?? this.title,
|
||||
headlineBold: headlineBold ?? this.headlineBold,
|
||||
headline: headline ?? this.headline,
|
||||
bodyBold: bodyBold ?? this.bodyBold,
|
||||
footnoteBold: footnoteBold ?? this.footnoteBold,
|
||||
footnote: footnote ?? this.footnote,
|
||||
captionBold: captionBold ?? this.captionBold,
|
||||
);
|
||||
}
|
||||
|
||||
TextTheme merge(TextTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
body: body?.merge(other.body) ?? other.body,
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
headlineBold:
|
||||
headlineBold?.merge(other.headlineBold) ?? other.headlineBold,
|
||||
headline: headline?.merge(other.headline) ?? other.headline,
|
||||
bodyBold: bodyBold?.merge(other.bodyBold) ?? other.bodyBold,
|
||||
footnoteBold:
|
||||
footnoteBold?.merge(other.footnoteBold) ?? other.footnoteBold,
|
||||
footnote: footnote?.merge(other.footnote) ?? other.footnote,
|
||||
captionBold: captionBold?.merge(other.captionBold) ?? other.captionBold,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ColorThemeType {
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
class ColorTheme {
|
||||
final Color black;
|
||||
final Color grey;
|
||||
final Color greyGainsboro;
|
||||
final Color greyWhisper;
|
||||
final Color whiteSmoke;
|
||||
final Color whiteSnow;
|
||||
final Color white;
|
||||
final Color blueAlice;
|
||||
final Color accentBlue;
|
||||
final Color accentRed;
|
||||
final Color accentGreen;
|
||||
final Effect borderTop;
|
||||
final Effect borderBottom;
|
||||
final Effect shadowIconButton;
|
||||
final Effect modalShadow;
|
||||
final Color highlight;
|
||||
final Color overlay;
|
||||
final Color overlayDark;
|
||||
final Gradient bgGradient;
|
||||
|
||||
ColorTheme.light({
|
||||
this.black = const Color(0xff000000),
|
||||
this.grey = const Color(0xff7a7a7a),
|
||||
this.greyGainsboro = const Color(0xffdbdbdb),
|
||||
this.greyWhisper = const Color(0xffecebeb),
|
||||
this.whiteSmoke = const Color(0xfff2f2f2),
|
||||
this.whiteSnow = const Color(0xfffcfcfc),
|
||||
this.white = const Color(0xffffffff),
|
||||
this.blueAlice = const Color(0xffe9f2ff),
|
||||
this.accentBlue = const Color(0xff005FFF),
|
||||
this.accentRed = const Color(0xffFF3842),
|
||||
this.accentGreen = const Color(0xff20E070),
|
||||
this.highlight = const Color(0xfffbf4dd),
|
||||
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
|
||||
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
|
||||
this.bgGradient = const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [const Color(0xfff7f7f7), const Color(0xfffcfcfc)],
|
||||
stops: [0, 1],
|
||||
),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
|
||||
this.borderBottom =
|
||||
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
|
||||
this.shadowIconButton = const Effect(
|
||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
|
||||
this.modalShadow = const Effect(
|
||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
|
||||
});
|
||||
|
||||
ColorTheme.dark({
|
||||
this.black = const Color(0xffffffff),
|
||||
this.grey = const Color(0xff7a7a7a),
|
||||
this.greyGainsboro = const Color(0xff2d2f2f),
|
||||
this.greyWhisper = const Color(0xff1c1e22),
|
||||
this.whiteSmoke = const Color(0xff13151b),
|
||||
this.whiteSnow = const Color(0xff070A0D),
|
||||
this.white = const Color(0xff101418),
|
||||
this.blueAlice = const Color(0xff00193D),
|
||||
this.accentBlue = const Color(0xff005FFF),
|
||||
this.accentRed = const Color(0xffFF3742),
|
||||
this.accentGreen = const Color(0xff20E070),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
|
||||
this.borderBottom =
|
||||
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
|
||||
this.shadowIconButton = const Effect(
|
||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
|
||||
this.modalShadow = const Effect(
|
||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
|
||||
this.highlight = const Color(0xff302d22),
|
||||
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
|
||||
this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6),
|
||||
this.bgGradient = const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [const Color(0xff101214), const Color(0xff070a0d)],
|
||||
stops: [0, 1],
|
||||
),
|
||||
});
|
||||
|
||||
ColorTheme copyWith({
|
||||
ColorThemeType type = ColorThemeType.light,
|
||||
Color black,
|
||||
Color grey,
|
||||
Color greyGainsboro,
|
||||
Color greyWhisper,
|
||||
Color whiteSmoke,
|
||||
Color whiteSnow,
|
||||
Color white,
|
||||
Color blueAlice,
|
||||
Color accentBlue,
|
||||
Color accentRed,
|
||||
Color accentGreen,
|
||||
Effect borderTop,
|
||||
Effect borderBottom,
|
||||
Effect shadowIconButton,
|
||||
Effect modalShadow,
|
||||
Color highlight,
|
||||
Color overlay,
|
||||
Color overlayDark,
|
||||
Gradient bgGradient,
|
||||
}) {
|
||||
return type == ColorThemeType.light
|
||||
? ColorTheme.light(
|
||||
black: black ?? this.black,
|
||||
grey: grey ?? this.grey,
|
||||
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
|
||||
greyWhisper: greyWhisper ?? this.greyWhisper,
|
||||
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
|
||||
whiteSnow: whiteSnow ?? this.whiteSnow,
|
||||
white: white ?? this.white,
|
||||
blueAlice: blueAlice ?? this.blueAlice,
|
||||
accentBlue: accentBlue ?? this.accentBlue,
|
||||
accentRed: accentRed ?? this.accentRed,
|
||||
accentGreen: accentGreen ?? this.accentGreen,
|
||||
borderTop: borderTop ?? this.borderTop,
|
||||
borderBottom: borderBottom ?? this.borderBottom,
|
||||
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
|
||||
modalShadow: modalShadow ?? this.modalShadow,
|
||||
highlight: highlight ?? this.highlight,
|
||||
overlay: overlay ?? this.overlay,
|
||||
overlayDark: overlayDark ?? this.overlayDark,
|
||||
bgGradient: bgGradient ?? this.bgGradient,
|
||||
)
|
||||
: ColorTheme.dark(
|
||||
black: black ?? this.black,
|
||||
grey: grey ?? this.grey,
|
||||
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
|
||||
greyWhisper: greyWhisper ?? this.greyWhisper,
|
||||
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
|
||||
whiteSnow: whiteSnow ?? this.whiteSnow,
|
||||
white: white ?? this.white,
|
||||
blueAlice: blueAlice ?? this.blueAlice,
|
||||
accentBlue: accentBlue ?? this.accentBlue,
|
||||
accentRed: accentRed ?? this.accentRed,
|
||||
accentGreen: accentGreen ?? this.accentGreen,
|
||||
borderTop: borderTop ?? this.borderTop,
|
||||
borderBottom: borderBottom ?? this.borderBottom,
|
||||
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
|
||||
modalShadow: modalShadow ?? this.modalShadow,
|
||||
highlight: highlight ?? this.highlight,
|
||||
overlay: overlay ?? this.overlay,
|
||||
overlayDark: overlayDark ?? this.overlayDark,
|
||||
bgGradient: bgGradient ?? this.bgGradient,
|
||||
);
|
||||
}
|
||||
|
||||
ColorTheme merge(ColorTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
black: other.black,
|
||||
grey: other.grey,
|
||||
greyGainsboro: other.greyGainsboro,
|
||||
greyWhisper: other.greyWhisper,
|
||||
whiteSmoke: other.whiteSmoke,
|
||||
whiteSnow: other.whiteSnow,
|
||||
white: other.white,
|
||||
blueAlice: other.blueAlice,
|
||||
accentBlue: other.accentBlue,
|
||||
accentRed: other.accentRed,
|
||||
accentGreen: other.accentGreen,
|
||||
highlight: other.highlight,
|
||||
overlay: other.overlay,
|
||||
overlayDark: other.overlayDark,
|
||||
bgGradient: other.bgGradient,
|
||||
borderTop: other.borderTop,
|
||||
borderBottom: other.borderBottom,
|
||||
shadowIconButton: other.shadowIconButton,
|
||||
modalShadow: other.modalShadow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel theme data
|
||||
class ChannelTheme {
|
||||
/// Theme of the [ChannelHeader] widget
|
||||
@@ -393,9 +653,6 @@ class ChannelTheme {
|
||||
/// Theme of the send button in [MessageInput]
|
||||
final ButtonThemeData messageInputButtonTheme;
|
||||
|
||||
/// Gradient of [MessageInput]
|
||||
final Gradient inputGradient;
|
||||
|
||||
/// Background color of [MessageInput]
|
||||
final Color inputBackground;
|
||||
|
||||
@@ -404,7 +661,6 @@ class ChannelTheme {
|
||||
this.messageInputButtonIconTheme,
|
||||
this.messageInputButtonTheme,
|
||||
this.inputBackground,
|
||||
this.inputGradient,
|
||||
});
|
||||
|
||||
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
|
||||
@@ -412,26 +668,29 @@ class ChannelTheme {
|
||||
ChannelHeaderTheme channelHeaderTheme,
|
||||
IconThemeData messageInputButtonIconTheme,
|
||||
ButtonThemeData messageInputButtonTheme,
|
||||
Gradient inputGradient,
|
||||
Color inputBackground,
|
||||
}) =>
|
||||
ChannelTheme(
|
||||
channelHeaderTheme: channelHeaderTheme?.copyWith(
|
||||
title: channelHeaderTheme?.title ?? this.channelHeaderTheme.title,
|
||||
lastMessageAt: channelHeaderTheme?.lastMessageAt ??
|
||||
this.channelHeaderTheme.lastMessageAt,
|
||||
avatarTheme: channelHeaderTheme?.avatarTheme ??
|
||||
this.channelHeaderTheme.avatarTheme,
|
||||
color: channelHeaderTheme?.color ?? this.channelHeaderTheme.color,
|
||||
) ??
|
||||
this.channelHeaderTheme,
|
||||
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
|
||||
messageInputButtonIconTheme:
|
||||
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
|
||||
messageInputButtonTheme:
|
||||
messageInputButtonTheme ?? this.messageInputButtonTheme,
|
||||
inputGradient: inputGradient ?? this.inputGradient,
|
||||
inputBackground: inputBackground ?? this.inputBackground,
|
||||
);
|
||||
|
||||
ChannelTheme merge(ChannelTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
|
||||
other.channelHeaderTheme,
|
||||
messageInputButtonIconTheme: messageInputButtonIconTheme
|
||||
?.merge(other.messageInputButtonIconTheme) ??
|
||||
other.messageInputButtonIconTheme,
|
||||
messageInputButtonTheme: other.messageInputButtonTheme,
|
||||
inputBackground: other.inputBackground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarTheme {
|
||||
@@ -451,6 +710,14 @@ class AvatarTheme {
|
||||
constraints: constraints ?? this.constraints,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
);
|
||||
|
||||
AvatarTheme merge(AvatarTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
constraints: other.constraints,
|
||||
borderRadius: other.borderRadius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageTheme {
|
||||
@@ -460,9 +727,9 @@ class MessageTheme {
|
||||
final TextStyle createdAt;
|
||||
final TextStyle replies;
|
||||
final Color messageBackgroundColor;
|
||||
final Color messageBorderColor;
|
||||
final Color reactionsBackgroundColor;
|
||||
final Color reactionsBorderColor;
|
||||
final Color replyThreadColor;
|
||||
final AvatarTheme avatarTheme;
|
||||
|
||||
const MessageTheme({
|
||||
@@ -471,9 +738,9 @@ class MessageTheme {
|
||||
this.messageAuthor,
|
||||
this.messageLinks,
|
||||
this.messageBackgroundColor,
|
||||
this.messageBorderColor,
|
||||
this.reactionsBackgroundColor,
|
||||
this.reactionsBorderColor,
|
||||
this.replyThreadColor,
|
||||
this.avatarTheme,
|
||||
this.createdAt,
|
||||
});
|
||||
@@ -485,10 +752,10 @@ class MessageTheme {
|
||||
TextStyle createdAt,
|
||||
TextStyle replies,
|
||||
Color messageBackgroundColor,
|
||||
Color messageBorderColor,
|
||||
AvatarTheme avatarTheme,
|
||||
Color reactionsBackgroundColor,
|
||||
Color reactionsBorderColor,
|
||||
Color replyThreadColor,
|
||||
}) =>
|
||||
MessageTheme(
|
||||
messageText: messageText ?? this.messageText,
|
||||
@@ -497,13 +764,31 @@ class MessageTheme {
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
messageBackgroundColor:
|
||||
messageBackgroundColor ?? this.messageBackgroundColor,
|
||||
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
replies: replies ?? this.replies,
|
||||
reactionsBackgroundColor:
|
||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
|
||||
);
|
||||
|
||||
MessageTheme merge(MessageTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
messageText: messageText?.merge(other.messageText) ?? other.messageText,
|
||||
messageAuthor:
|
||||
messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor,
|
||||
messageLinks:
|
||||
messageLinks?.merge(other.messageLinks) ?? other.messageLinks,
|
||||
createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt,
|
||||
replies: replies?.merge(other.replies) ?? other.replies,
|
||||
messageBackgroundColor: other.messageBackgroundColor,
|
||||
messageBorderColor: other.messageBorderColor,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
reactionsBackgroundColor: other.reactionsBackgroundColor,
|
||||
reactionsBorderColor: other.reactionsBorderColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPreviewTheme {
|
||||
@@ -512,6 +797,7 @@ class ChannelPreviewTheme {
|
||||
final TextStyle lastMessageAt;
|
||||
final AvatarTheme avatarTheme;
|
||||
final Color unreadCounterColor;
|
||||
final double indicatorIconSize;
|
||||
|
||||
const ChannelPreviewTheme({
|
||||
this.title,
|
||||
@@ -519,6 +805,7 @@ class ChannelPreviewTheme {
|
||||
this.lastMessageAt,
|
||||
this.avatarTheme,
|
||||
this.unreadCounterColor,
|
||||
this.indicatorIconSize,
|
||||
});
|
||||
|
||||
ChannelPreviewTheme copyWith({
|
||||
@@ -527,6 +814,7 @@ class ChannelPreviewTheme {
|
||||
TextStyle lastMessageAt,
|
||||
AvatarTheme avatarTheme,
|
||||
Color unreadCounterColor,
|
||||
double indicatorIconSize,
|
||||
}) =>
|
||||
ChannelPreviewTheme(
|
||||
title: title ?? this.title,
|
||||
@@ -534,7 +822,20 @@ class ChannelPreviewTheme {
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
|
||||
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
|
||||
);
|
||||
|
||||
ChannelPreviewTheme merge(ChannelPreviewTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
|
||||
lastMessageAt:
|
||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
unreadCounterColor: other.unreadCounterColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelHeaderTheme {
|
||||
@@ -562,4 +863,46 @@ class ChannelHeaderTheme {
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
ChannelHeaderTheme merge(ChannelHeaderTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
lastMessageAt:
|
||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
color: other.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Effect {
|
||||
final double sigmaX;
|
||||
final double sigmaY;
|
||||
final Color color;
|
||||
final double alpha;
|
||||
final double blur;
|
||||
|
||||
const Effect({
|
||||
this.sigmaX,
|
||||
this.sigmaY,
|
||||
this.color,
|
||||
this.alpha,
|
||||
this.blur,
|
||||
});
|
||||
|
||||
Effect copyWith({
|
||||
double sigmaX,
|
||||
double sigmaY,
|
||||
Color color,
|
||||
double alpha,
|
||||
double blur,
|
||||
}) =>
|
||||
Effect(
|
||||
sigmaX: sigmaX ?? this.sigmaX,
|
||||
sigmaY: sigmaY ?? this.sigmaY,
|
||||
color: color ?? this.color,
|
||||
alpha: color ?? this.alpha,
|
||||
blur: blur ?? this.blur,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
|
||||
const StreamSvgIcon({
|
||||
this.assetName,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.width = 24,
|
||||
this.height = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -194,6 +194,30 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.checkAll({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_check_all.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.checkSend({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_check_send.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.penWrite({
|
||||
double size,
|
||||
Color color,
|
||||
@@ -326,6 +350,18 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.reply({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_curve_line_left_up_big.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.edit({
|
||||
double size,
|
||||
Color color,
|
||||
@@ -757,4 +793,40 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.error({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_error.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.circle_up({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_circle_up.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_user_settings({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_user_settings.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
|
||||
///
|
||||
class Swipeable extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Widget backgroundIcon;
|
||||
final VoidCallback onSwipeStart;
|
||||
final VoidCallback onSwipeCancel;
|
||||
final VoidCallback onSwipeEnd;
|
||||
final double threshold;
|
||||
|
||||
///
|
||||
const Swipeable({
|
||||
@required this.child,
|
||||
@required this.backgroundIcon,
|
||||
this.onSwipeStart,
|
||||
this.onSwipeCancel,
|
||||
this.onSwipeEnd,
|
||||
this.threshold = 82.0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SwipeableState();
|
||||
}
|
||||
|
||||
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
||||
double _dragExtent = 0.0;
|
||||
AnimationController _moveController;
|
||||
AnimationController _iconMoveController;
|
||||
Animation<Offset> _moveAnimation;
|
||||
Animation<Offset> _iconTransitionAnimation;
|
||||
Animation<double> _iconFadeAnimation;
|
||||
bool _pastThreshold = false;
|
||||
|
||||
final _animationDuration = const Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_moveController =
|
||||
AnimationController(duration: _animationDuration, vsync: this);
|
||||
_iconMoveController =
|
||||
AnimationController(duration: _animationDuration, vsync: this);
|
||||
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
|
||||
.animate(_moveController);
|
||||
_iconTransitionAnimation =
|
||||
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0))
|
||||
.animate(_moveController);
|
||||
_iconFadeAnimation =
|
||||
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController);
|
||||
|
||||
final controllerValue = 0.0;
|
||||
_moveController.animateTo(controllerValue);
|
||||
_iconMoveController.animateTo(controllerValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_moveController.dispose();
|
||||
_iconMoveController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
if (widget.onSwipeStart != null) {
|
||||
widget.onSwipeStart();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
final delta = details.primaryDelta;
|
||||
_dragExtent += delta;
|
||||
|
||||
if (_dragExtent.isNegative) return;
|
||||
|
||||
var movePastThresholdPixels = widget.threshold;
|
||||
var newPos = _dragExtent.abs() / context.size.width;
|
||||
|
||||
if (_dragExtent.abs() > movePastThresholdPixels) {
|
||||
// how many "thresholds" past the threshold we are. 1 = the threshold 2
|
||||
// = two thresholds.
|
||||
var n = _dragExtent.abs() / movePastThresholdPixels;
|
||||
|
||||
// Take the number of thresholds past the threshold, and reduce this
|
||||
// number
|
||||
var reducedThreshold = math.pow(n, 0.3);
|
||||
|
||||
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
|
||||
newPos = adjustedPixelPos / context.size.width;
|
||||
|
||||
if (_dragExtent > 0 && !_pastThreshold) {
|
||||
_iconMoveController.value = 1;
|
||||
_pastThreshold = true;
|
||||
}
|
||||
} else {
|
||||
// Send a cancel event if the user has swiped back underneath the
|
||||
// threshold
|
||||
if (_pastThreshold && widget.onSwipeCancel != null) {
|
||||
widget.onSwipeCancel();
|
||||
}
|
||||
_pastThreshold = false;
|
||||
}
|
||||
if (!_pastThreshold || newPos < _moveController.value) {
|
||||
_iconMoveController.value = newPos;
|
||||
}
|
||||
_moveController.value = newPos;
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
_moveController.animateTo(0.0, duration: _animationDuration);
|
||||
_iconMoveController.animateTo(0.0, duration: _animationDuration);
|
||||
_dragExtent = 0.0;
|
||||
if (_pastThreshold && widget.onSwipeEnd != null) {
|
||||
widget.onSwipeEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: _handleDragStart,
|
||||
onHorizontalDragUpdate: _handleDragUpdate,
|
||||
onHorizontalDragEnd: _handleDragEnd,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
SlideTransition(
|
||||
position: _iconTransitionAnimation,
|
||||
child: Row(
|
||||
children: [
|
||||
FadeTransition(
|
||||
opacity: _iconFadeAnimation,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
child: widget.backgroundIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SlideTransition(
|
||||
position: _moveAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,17 +51,21 @@ class UrlAttachment extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
hostDisplayName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF006CFF),
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
color: Color(0xFFE9F2FF),
|
||||
color: StreamChatTheme.of(context).colorTheme.blueAlice,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -81,18 +85,18 @@ class UrlAttachment extends StatelessWidget {
|
||||
urlAttachment.title.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
if (urlAttachment.text != null)
|
||||
Text(
|
||||
urlAttachment.text,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ class UserAvatar extends StatelessWidget {
|
||||
this.borderRadius,
|
||||
this.onlineIndicatorAlignment = Alignment.topRight,
|
||||
this.selected = false,
|
||||
this.selectionColor = const Color(0xFF006CFF),
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -47,7 +47,7 @@ class UserAvatar extends StatelessWidget {
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: streamChatTheme.accentColor,
|
||||
color: streamChatTheme.colorTheme.accentBlue,
|
||||
),
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
@@ -61,13 +61,17 @@ class UserAvatar extends StatelessWidget {
|
||||
: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
avatar = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
color: selectionColor,
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: avatar,
|
||||
@@ -96,10 +100,10 @@ class UserAvatar extends StatelessWidget {
|
||||
),
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
color: Color(0xff20E070),
|
||||
color: streamChatTheme.colorTheme.accentGreen,
|
||||
),
|
||||
),
|
||||
color: Colors.white,
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -73,25 +73,25 @@ class UserItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
trailing: selected
|
||||
? CircleAvatar(
|
||||
child: StreamSvgIcon.check(
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 10,
|
||||
? StreamSvgIcon.checkSend(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
user.name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
),
|
||||
subtitle: showLastOnline ? _buildLastActive(context) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastActive(context) {
|
||||
return user.online == true
|
||||
? Text('Online')
|
||||
: Text('Last online ${Jiffy(user.lastActive).fromNow()}');
|
||||
return Text(
|
||||
user.online == true
|
||||
? 'Online'
|
||||
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,15 +356,17 @@ class _UserListViewState extends State<UserListView>
|
||||
headerItem: (header) {
|
||||
return Container(
|
||||
key: ValueKey<String>('HEADER-$header'),
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
|
||||
child: Text(
|
||||
header,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(0.3)),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.5,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -449,7 +451,10 @@ class _UserListViewState extends State<UserListView>
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
@@ -471,9 +476,7 @@ class _UserListViewState extends State<UserListView>
|
||||
Widget _separatorBuilder(context, i) {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
@@ -25,7 +27,7 @@ Future<bool> showConfirmationDialog(
|
||||
String cancelText,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
@@ -45,7 +47,7 @@ Future<bool> showConfirmationDialog(
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
@@ -55,7 +57,8 @@ Future<bool> showConfirmationDialog(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color: Color(0xffe6e6e6),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
@@ -65,7 +68,10 @@ Future<bool> showConfirmationDialog(
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -93,11 +99,119 @@ Future<bool> showConfirmationDialog(
|
||||
String getRandomPicUrl(User user) =>
|
||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
||||
|
||||
/// List extension
|
||||
extension ListX<T> on List<T> {
|
||||
/// Insert any item<T> inBetween the list items
|
||||
List<T> insertBetween(T item) => expand((e) sync* {
|
||||
yield item;
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
/// Get websiteName from [hostName]
|
||||
String getWebsiteName(String hostName) {
|
||||
switch (hostName) {
|
||||
case 'reddit':
|
||||
return 'Reddit';
|
||||
case 'youtube':
|
||||
return 'Youtube';
|
||||
case 'wikipedia':
|
||||
return 'Wikipedia';
|
||||
case 'twitter':
|
||||
return 'Twitter';
|
||||
case 'facebook':
|
||||
return 'Facebook';
|
||||
case 'amazon':
|
||||
return 'Amazon';
|
||||
case 'yelp':
|
||||
return 'Yelp';
|
||||
case 'imdb':
|
||||
return 'IMDB';
|
||||
case 'pinterest':
|
||||
return 'Pinterest';
|
||||
case 'tripadvisor':
|
||||
return 'TripAdvisor';
|
||||
case 'instagram':
|
||||
return 'Instagram';
|
||||
case 'walmart':
|
||||
return 'Walmart';
|
||||
case 'craigslist':
|
||||
return 'Craigslist';
|
||||
case 'ebay':
|
||||
return 'eBay';
|
||||
case 'linkedin':
|
||||
return 'LinkedIn';
|
||||
case 'google':
|
||||
return 'Google';
|
||||
case 'apple':
|
||||
return 'Apple';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
String getSizeText(int bytes) {
|
||||
if (bytes == null) {
|
||||
return 'Size N/A';
|
||||
}
|
||||
|
||||
if (bytes <= 1000) {
|
||||
return '${bytes} bytes';
|
||||
} else if (bytes <= 100000) {
|
||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
||||
} else {
|
||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
StreamSvgIcon getFileTypeImage(String type) {
|
||||
switch (type) {
|
||||
case '7z':
|
||||
return StreamSvgIcon.filetype_7z();
|
||||
break;
|
||||
case 'csv':
|
||||
return StreamSvgIcon.filetype_csv();
|
||||
break;
|
||||
case 'doc':
|
||||
return StreamSvgIcon.filetype_doc();
|
||||
break;
|
||||
case 'docx':
|
||||
return StreamSvgIcon.filetype_docx();
|
||||
break;
|
||||
case 'html':
|
||||
return StreamSvgIcon.filetype_html();
|
||||
break;
|
||||
case 'md':
|
||||
return StreamSvgIcon.filetype_md();
|
||||
break;
|
||||
case 'odt':
|
||||
return StreamSvgIcon.filetype_odt();
|
||||
break;
|
||||
case 'pdf':
|
||||
return StreamSvgIcon.filetype_pdf();
|
||||
break;
|
||||
case 'ppt':
|
||||
return StreamSvgIcon.filetype_ppt();
|
||||
break;
|
||||
case 'pptx':
|
||||
return StreamSvgIcon.filetype_pptx();
|
||||
break;
|
||||
case 'rar':
|
||||
return StreamSvgIcon.filetype_rar();
|
||||
break;
|
||||
case 'rtf':
|
||||
return StreamSvgIcon.filetype_rtf();
|
||||
break;
|
||||
case 'tar':
|
||||
return StreamSvgIcon.filetype_tar();
|
||||
break;
|
||||
case 'txt':
|
||||
return StreamSvgIcon.filetype_txt();
|
||||
break;
|
||||
case 'xls':
|
||||
return StreamSvgIcon.filetype_xls();
|
||||
break;
|
||||
case 'xlsx':
|
||||
return StreamSvgIcon.filetype_xlsx();
|
||||
break;
|
||||
case 'zip':
|
||||
return StreamSvgIcon.filetype_zip();
|
||||
break;
|
||||
default:
|
||||
return StreamSvgIcon.filetype_Generic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class VideoAttachment extends StatefulWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final Message message;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
VideoAttachment({
|
||||
Key key,
|
||||
@@ -21,6 +22,7 @@ class VideoAttachment extends StatefulWidget {
|
||||
@required this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -95,6 +97,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
|
||||
userName: widget.message.user.name,
|
||||
sentAt: widget.message.createdAt,
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -43,3 +43,6 @@ export 'src/message_search_bloc.dart';
|
||||
export 'src/message_search_item.dart';
|
||||
export 'src/message_search_list_view.dart';
|
||||
export 'src/unread_indicator.dart';
|
||||
export 'src/option_list_tile.dart';
|
||||
export 'src/channel_file_display_screen.dart';
|
||||
export 'src/channel_media_display_screen.dart';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11ZM12 9C13.1046 9 14 8.10457 14 7C14 5.89543 13.1046 5 12 5C10.8954 5 10 5.89543 10 7C10 8.10457 10.8954 9 12 9Z" fill="#7A7A7A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 12C6.4686 12 4 15.6324 4 18C4 18.5523 3.55228 19 3 19C2.44771 19 2 18.5523 2 18C2 14.3676 5.5314 10 12 10C13.9955 10 15.7139 10.4121 17.1402 11.0991C17.6378 11.3387 17.8469 11.9363 17.6072 12.4339C17.3676 12.9315 16.7699 13.1406 16.2724 12.9009C15.1351 12.3532 13.716 12 12 12Z" fill="#7A7A7A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14C18.2239 14 18 14.2239 18 14.5V15.2679L17.3349 14.884C17.0958 14.7459 16.79 14.8278 16.6519 15.067L16.1519 15.933C16.0139 16.1722 16.0958 16.478 16.3349 16.616L17 17L16.3349 17.384C16.0958 17.522 16.0138 17.8278 16.1519 18.067L16.6519 18.933C16.79 19.1722 17.0958 19.2541 17.3349 19.116L18 18.732V19.5C18 19.7761 18.2239 20 18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V18.732L20.6651 19.116C20.9042 19.2541 21.21 19.1722 21.3481 18.933L21.8481 18.067C21.9861 17.8278 21.9042 17.522 21.6651 17.384L21 17L21.665 16.616C21.9042 16.478 21.9861 16.1722 21.8481 15.933L21.3481 15.067C21.21 14.8278 20.9042 14.7459 20.665 14.884L20 15.2679V14.5C20 14.2239 19.7761 14 19.5 14H18.5ZM19 18C19.5523 18 20 17.5523 20 17C20 16.4477 19.5523 16 19 16C18.4477 16 18 16.4477 18 17C18 17.5523 18.4477 18 19 18Z" fill="#7A7A7A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -24,11 +24,11 @@ dependencies:
|
||||
url_launcher: ^5.4.11
|
||||
emojis: ^0.9.3
|
||||
video_player: ^1.0.0
|
||||
chewie: ^0.10.4
|
||||
chewie: ^0.12.1
|
||||
file_picker: ^2.0.12
|
||||
image_picker: ^0.6.7+2
|
||||
flutter_keyboard_visibility: ^3.3.0
|
||||
stream_chat: ^0.2.20
|
||||
flutter_keyboard_visibility: ^4.0.2
|
||||
stream_chat: ^0.2.23
|
||||
mime: ^0.9.6+3
|
||||
video_compress: ^2.1.1
|
||||
visibility_detector: ^0.1.5
|
||||
|
||||
@@ -71,6 +71,7 @@ void main() {
|
||||
showCopyMessage: false,
|
||||
showDeleteMessage: false,
|
||||
showReply: false,
|
||||
showThreadReply: false,
|
||||
message: Message(
|
||||
text: 'test',
|
||||
user: User(
|
||||
@@ -87,6 +88,7 @@ void main() {
|
||||
|
||||
await tester.pump(Duration(milliseconds: 1000));
|
||||
expect(find.byKey(Key('MessageWidget')), findsOneWidget);
|
||||
expect(find.text('Reply'), findsNothing);
|
||||
expect(find.text('Thread reply'), findsNothing);
|
||||
expect(find.text('Edit message'), findsNothing);
|
||||
expect(find.text('Delete message'), findsNothing);
|
||||
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
),
|
||||
latestReactions: [
|
||||
Reaction(
|
||||
type: 'thumbs_up',
|
||||
type: 'like',
|
||||
user: User(id: testUserId),
|
||||
),
|
||||
Reaction(
|
||||
|
||||
@@ -31,7 +31,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'it should show a thumb up',
|
||||
'it should show a like',
|
||||
(WidgetTester tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
@@ -50,7 +50,7 @@ void main() {
|
||||
child: ReactionBubble(
|
||||
reactions: [
|
||||
Reaction(
|
||||
type: 'thumbs_up',
|
||||
type: 'like',
|
||||
user: User(id: 'test'),
|
||||
),
|
||||
],
|
||||
@@ -86,7 +86,7 @@ void main() {
|
||||
child: ReactionBubble(
|
||||
reactions: [
|
||||
Reaction(
|
||||
type: 'thumbs_up',
|
||||
type: 'like',
|
||||
user: User(id: 'test'),
|
||||
),
|
||||
Reaction(
|
||||
|
||||