Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/mention-screen

 Conflicts:
	lib/src/message_list_view.dart
This commit is contained in:
Sahil Kumar
2021-01-07 11:00:31 +05:30
87 changed files with 6038 additions and 3166 deletions
+61 -61
View File
@@ -1,63 +1,63 @@
include: package:pedantic/analysis_options.yaml #include: package:pedantic/analysis_options.yaml
#
analyzer: #analyzer:
enable-experiment: # enable-experiment:
- extension-methods # - extension-methods
exclude: # exclude:
- lib/**/*.g.dart # - lib/**/*.g.dart
- example/** # - example/**
#
linter: #linter:
rules: # rules:
# these rules are documented on and in the same order as # # these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier # # the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml # # https://github.com/dart-lang/linter/blob/master/example/all.yaml
# - always_declare_return_types # # - always_declare_return_types
# - always_specify_types # # - always_specify_types
# - annotate_overrides # # - annotate_overrides
# - avoid_as # # - avoid_as
- avoid_empty_else # - avoid_empty_else
- avoid_init_to_null # - avoid_init_to_null
- avoid_return_types_on_setters # - avoid_return_types_on_setters
- avoid_web_libraries_in_flutter # - avoid_web_libraries_in_flutter
- await_only_futures # - await_only_futures
- camel_case_types # - camel_case_types
- cancel_subscriptions # - cancel_subscriptions
- close_sinks # - close_sinks
# - comment_references # we do not presume as to what people want to reference in their dartdocs # # - 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 # # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally # - control_flow_in_finally
- empty_constructor_bodies # - empty_constructor_bodies
- empty_statements # - empty_statements
- hash_and_equals # - hash_and_equals
- implementation_imports # - implementation_imports
# - invariant_booleans # # - invariant_booleans
# - iterable_contains_unrelated_type # # - iterable_contains_unrelated_type
- library_names # - library_names
# - library_prefixes # # - library_prefixes
# - list_remove_unrelated_type # # - list_remove_unrelated_type
# - literal_only_boolean_expressions # # - literal_only_boolean_expressions
- non_constant_identifier_names # - non_constant_identifier_names
# - one_member_abstracts # # - one_member_abstracts
# - only_throw_errors # # - only_throw_errors
# - overridden_fields # # - overridden_fields
# - package_api_docs ## - package_api_docs
- package_names # - package_names
- package_prefixed_library_names # - package_prefixed_library_names
- prefer_is_not_empty # - prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32 # # - prefer_mixin # https://github.com/dart-lang/language/issues/32
- public_member_api_docs # - public_member_api_docs
- slash_for_doc_comments # - slash_for_doc_comments
# - sort_constructors_first # # - sort_constructors_first
# - sort_unnamed_constructors_first # # - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2 # # - super_goes_last # no longer needed w/ Dart 2
- test_types_in_equals # - test_types_in_equals
- throw_in_finally # - throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types # # - type_annotate_public_apis # subset of always_specify_types
- type_init_formals # - type_init_formals
# - unawaited_futures # # - unawaited_futures
- unnecessary_brace_in_string_interps # - unnecessary_brace_in_string_interps
- unnecessary_getters_setters # - unnecessary_getters_setters
- unnecessary_statements # - unnecessary_statements
- unrelated_type_equality_checks - unrelated_type_equality_checks
- valid_regexps - valid_regexps
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+1 -1
View File
@@ -1 +1 @@
13a41b8138c4868054e44b5158f3bdd6 c770358a10272b4ee1cdc2eb432445f6
+1 -1
View File
@@ -265,7 +265,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2 FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
FirebaseInstanceID: bd3ffc24367f901a43c063b36c640b345a4a5dd1 FirebaseInstanceID: bd3ffc24367f901a43c063b36c640b345a4a5dd1
FirebaseMessaging: 5eca4ef173de76253352511aafef774caa1cba2a FirebaseMessaging: 5eca4ef173de76253352511aafef774caa1cba2a
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
flutter_apns: ddc629f26016140bf52165040b0a8e8869f9ce32 flutter_apns: ddc629f26016140bf52165040b0a8e8869f9ce32
flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68 flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
@@ -314,7 +314,6 @@
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
"${PODS_ROOT}/../Flutter/Flutter.framework",
"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.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}/DKImagePickerController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.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}/GoogleUtilities.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
-2
View File
@@ -58,8 +58,6 @@
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true/>
</dict> </dict>
+17 -5
View File
@@ -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>
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000521"> <testcase classname="fastlane.lanes" name="1: default_platform" time="0.000176">
</testcase> </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>
<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>
<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'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing'&#10;Fastfile:28:in `block (2 levels) in parsing_binding'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane.rb:33:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/bin/fastlane:23:in `&lt;top (required)&gt;'&#10;/usr/local/bin/fastlane:23:in `load'&#10;/usr/local/bin/fastlane:23:in `&lt;top (required)&gt;'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:30:in `block in &lt;top (required)&gt;'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'&#10;/Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:22:in `&lt;top (required)&gt;'&#10;/usr/local/bin/bundle:23:in `load'&#10;/usr/local/bin/bundle:23:in `&lt;main&gt;'&#10;&#10;Error building the application - see the log above" />
</testcase> </testcase>
+50 -66
View File
@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'choose_user_page.dart';
import 'main.dart'; import 'main.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
import 'choose_user_page.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AdvancedOptionsPage extends StatefulWidget { class AdvancedOptionsPage extends StatefulWidget {
@override @override
@@ -34,20 +34,23 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
resizeToAvoidBottomPadding: false, resizeToAvoidBottomPadding: false,
appBar: AppBar( appBar: AppBar(
backgroundColor: StreamChatTheme.of(context).primaryColor, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
elevation: 1, elevation: 1,
centerTitle: true, centerTitle: true,
brightness: Theme.of(context).brightness,
title: Text( title: Text(
'Advanced Options', 'Advanced Options',
style: Theme.of(context).textTheme.subtitle1.copyWith( style: StreamChatTheme.of(context)
fontWeight: FontWeight.bold, .textTheme
), .headlineBold
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
), ),
leading: IconButton( leading: IconButton(
icon: StreamSvgIcon.left( icon: StreamSvgIcon.left(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@@ -57,15 +60,11 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
body: Builder( body: Builder(
builder: (context) { builder: (context) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
16,
16,
16,
0,
),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextFormField( TextFormField(
controller: _apiKeyController, controller: _apiKeyController,
@@ -87,31 +86,31 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
decoration: InputDecoration( decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 0), errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle( labelStyle: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _apiKeyError != null color: _apiKeyError != null
? Color(0xffff3742) ? StreamChatTheme.of(context).colorTheme.accentRed
: Colors.black.withOpacity(.5), : StreamChatTheme.of(context).colorTheme.grey,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: Color(0xffF5F5F5), fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: labelText:
'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}', 'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}',
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
), ),
Padding( SizedBox(height: 8),
padding: const EdgeInsets.only(top: 8.0), TextFormField(
child: TextFormField(
controller: _userIdController, controller: _userIdController,
onChanged: (_) { onChanged: (_) {
if (_userIdError != null) { if (_userIdError != null) {
@@ -131,32 +130,31 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
decoration: InputDecoration( decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 0), errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle( labelStyle: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: _userIdError != null color: _userIdError != null
? Color(0xffff3742) ? StreamChatTheme.of(context).colorTheme.accentRed
: Colors.black.withOpacity(.5), : StreamChatTheme.of(context).colorTheme.grey,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: Color(0xffF5F5F5), fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: labelText:
'User ID ${_userIdError != null ? ': $_userIdError' : ''}', 'User ID ${_userIdError != null ? ': $_userIdError' : ''}',
), ),
), ),
), SizedBox(height: 8),
Padding( TextFormField(
padding: const EdgeInsets.only(top: 8.0),
child: TextFormField(
onChanged: (_) { onChanged: (_) {
if (_userTokenError != null) { if (_userTokenError != null) {
setState(() { setState(() {
@@ -176,67 +174,53 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
decoration: InputDecoration( decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 0), errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle( labelStyle: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: _userTokenError != null color: _userTokenError != null
? Color(0xffff3742) ? StreamChatTheme.of(context).colorTheme.accentRed
: Colors.black.withOpacity(.5), : StreamChatTheme.of(context).colorTheme.grey,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: Color(0xffF5F5F5), fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: labelText:
'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}', 'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}',
), ),
), ),
), SizedBox(height: 8),
Padding( TextFormField(
padding: const EdgeInsets.only(top: 8.0),
child: TextFormField(
controller: _usernameController, controller: _usernameController,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
decoration: InputDecoration( decoration: InputDecoration(
labelStyle: TextStyle( labelStyle: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context).colorTheme.grey,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: Color(0xffF5F5F5), fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: 'Username (optional)', labelText: 'Username (optional)',
), ),
), ),
), Spacer(),
Expanded( RaisedButton(
child: Align( elevation: 0,
alignment: Alignment.bottomCenter, child: Text('Login', style: TextStyle(fontSize: 16)),
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 { onPressed: () async {
if (loading) { if (loading) {
return; return;
@@ -251,11 +235,15 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
showDialog( showDialog(
barrierDismissible: false, barrierDismissible: false,
context: context, context: context,
barrierColor:
StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) => Center( builder: (context) => Center(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
), ),
height: 100, height: 100,
width: 100, width: 100,
@@ -269,8 +257,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
final client = Client( final client = Client(
apiKey, apiKey,
logLevel: Level.INFO, logLevel: Level.INFO,
showLocalNotification: showLocalNotification: (!kIsWeb && Platform.isAndroid)
(!kIsWeb && Platform.isAndroid)
? showLocalNotification ? showLocalNotification
: null, : null,
persistenceEnabled: true, persistenceEnabled: true,
@@ -328,8 +315,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
return MaterialApp( return MaterialApp(
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented themeMode: ThemeMode.system,
themeMode: ThemeMode.light,
builder: (context, widget) { builder: (context, widget) {
return StreamChat( return StreamChat(
child: widget, child: widget,
@@ -345,8 +331,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
} }
}, },
), ),
),
),
StreamVersion(), StreamVersion(),
], ],
), ),
@@ -3,7 +3,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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 /// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget { class ChatInfoScreen extends StatefulWidget {
@@ -21,16 +24,18 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
return Scaffold( return Scaffold(
backgroundColor: Color(0xFFe6e6e6), backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
body: ListView( body: ListView(
children: [ children: [
_buildUserHeader(), _buildUserHeader(),
SizedBox( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
_buildOptionListTiles(), _buildOptionListTiles(),
SizedBox( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
if ([ if ([
'admin', 'admin',
@@ -47,7 +52,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Widget _buildUserHeader() { Widget _buildUserHeader() {
return Material( return Material(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: SafeArea( child: SafeArea(
child: Stack( child: Stack(
children: [ children: [
@@ -74,14 +79,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
SizedBox(height: 7.0), SizedBox(height: 7.0),
_buildConnectedTitleState(), _buildConnectedTitleState(),
SizedBox(height: 15.0), SizedBox(height: 15.0),
_OptionListTile( OptionListTile(
title: '@${widget.user.id}', title: '@${widget.user.id}',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
trailing: Padding( trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
widget.user.name, widget.user.name,
style: TextStyle( style: TextStyle(
color: Colors.black.withOpacity(0.5), fontSize: 16.0), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
fontSize: 16.0),
), ),
), ),
onTap: () {}, onTap: () {},
@@ -92,7 +102,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
top: 21, top: 21,
left: 16, left: 16,
child: InkWell( child: InkWell(
child: StreamSvgIcon.left(), child: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black,
),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@@ -113,7 +125,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// title: 'Notifications', // title: 'Notifications',
// leading: StreamSvgIcon.Icon_notification( // leading: StreamSvgIcon.Icon_notification(
// size: 24.0, // size: 24.0,
// color: Colors.black.withOpacity(0.5), // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ), // ),
// trailing: CupertinoSwitch( // trailing: CupertinoSwitch(
// value: true, // value: true,
@@ -124,11 +136,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
StreamBuilder<bool>( StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream, stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return _OptionListTile( return OptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
title: 'Mute user', title: 'Mute user',
leading: StreamSvgIcon.mute( titleTextStyle: StreamChatTheme.of(context).textTheme.body,
size: 23.0, leading: Padding(
color: Colors.black.withOpacity(0.5), 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 trailing: snapshot.data == null
? CircularProgressIndicator() ? CircularProgressIndicator()
@@ -149,7 +169,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// title: 'Block User', // title: 'Block User',
// leading: StreamSvgIcon.Icon_user_delete( // leading: StreamSvgIcon.Icon_user_delete(
// size: 24.0, // size: 24.0,
// color: Colors.black.withOpacity(0.5), // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ), // ),
// trailing: CupertinoSwitch( // trailing: CupertinoSwitch(
// value: widget.user.banned, // value: widget.user.banned,
@@ -163,37 +183,118 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// ), // ),
// onTap: () {}, // onTap: () {},
// ), // ),
_OptionListTile( OptionListTile(
title: 'Photos & Videos', title: 'Photos & Videos',
leading: StreamSvgIcon.pictures( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
size: 32.0, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
color: Colors.black.withOpacity(0.5), 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: () { onTap: () {
Navigator.push(context, final channel = StreamChannel.of(context).channel;
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
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', title: 'Files',
leading: StreamSvgIcon.files( 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, size: 32.0,
color: Colors.black.withOpacity(0.5), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
), ),
trailing: StreamSvgIcon.right(),
onTap: () { onTap: () {
Navigator.push(context, final channel = StreamChannel.of(context).channel;
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
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', title: 'Shared groups',
leading: StreamSvgIcon.Icon_group( 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, size: 24.0,
color: Colors.black.withOpacity(0.5), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
), ),
trailing: StreamSvgIcon.right(),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -207,16 +308,23 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
Widget _buildDeleteListTile() { Widget _buildDeleteListTile() {
return _OptionListTile( return OptionListTile(
title: 'Delete', title: 'Delete Conversation',
leading: StreamSvgIcon.delete( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
color: Colors.red, 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, size: 24.0,
), ),
),
onTap: () { onTap: () {
_showDeleteDialog(); _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?', question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: Colors.red, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
); );
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
@@ -248,12 +356,20 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
if (otherMember.online) { if (otherMember.online) {
alternativeWidget = Text( alternativeWidget = Text(
'Online', 'Online',
style: TextStyle(color: Colors.black.withOpacity(0.5)), style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
); );
} else { } else {
alternativeWidget = Text( alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}', '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( child: Material(
shape: CircleBorder(), shape: CircleBorder(),
color: Color(0xff20E070), color: StreamChatTheme.of(context).colorTheme.accentGreen,
), ),
), ),
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
alternativeWidget, 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 { class _SharedGroupsScreen extends StatefulWidget {
final User mainUser; final User mainUser;
final User otherUser; final User otherUser;
@@ -365,14 +415,16 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
var chat = StreamChat.of(context); var chat = StreamChat.of(context);
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
centerTitle: true, centerTitle: true,
title: Text( title: Text(
'Shared Groups', 'Shared Groups',
style: TextStyle(color: Colors.black, fontSize: 16.0), style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16.0),
), ),
leading: Center( leading: Center(
child: InkWell( child: InkWell(
@@ -381,7 +433,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
}, },
child: Container( child: Container(
child: StreamSvgIcon.left( child: StreamSvgIcon.left(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0, size: 24.0,
), ),
width: 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>>( body: FutureBuilder<List<Channel>>(
future: chat.client.queryChannels( future: chat.client.queryChannels(
@@ -409,12 +461,46 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
}, },
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == null) { if (!snapshot.hasData) {
return Center( return Center(
child: CircularProgressIndicator(), 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( return ListView.builder(
itemCount: snapshot.data.length, itemCount: snapshot.data.length,
itemBuilder: (context, position) { itemBuilder: (context, position) {
@@ -488,7 +574,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'${channel.memberCount} members', '${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( Container(
height: 1.0, 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,
),
);
}
}
+25 -11
View File
@@ -66,7 +66,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
onTap: _pauseItemAddition ? resumeItemAddition : null, onTap: _pauseItemAddition ? resumeItemAddition : null,
child: Material( child: Material(
elevation: 1, elevation: 1,
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
child: Container( child: Container(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), 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), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text( child: Text(
'TO:', 'TO:',
style: TextStyle( style: StreamChatTheme.of(context)
fontSize: 12, .textTheme
color: Colors.black.withOpacity(0.5), .footnote
), .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
), ),
), ),
SizedBox(width: 12), SizedBox(width: 12),
@@ -111,10 +115,14 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
disabledBorder: InputBorder.none, disabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.only(top: 4.0), contentPadding: const EdgeInsets.only(top: 4.0),
hintText: widget.hint, hintText: widget.hint,
hintStyle: TextStyle( hintStyle: StreamChatTheme.of(context)
color: Colors.black.withOpacity(0.5), .textTheme
fontSize: 14, .body
), .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
), ),
), ),
], ],
@@ -126,11 +134,17 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
child: IconButton( child: IconButton(
icon: _chips.isEmpty icon: _chips.isEmpty
? StreamSvgIcon.user( ? StreamSvgIcon.user(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 24, size: 24,
) )
: StreamSvgIcon.userAdd( : StreamSvgIcon.userAdd(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 24, size: 24,
), ),
onPressed: resumeItemAddition, onPressed: resumeItemAddition,
+43 -25
View File
@@ -1,5 +1,3 @@
import 'package:example/advanced_options_page.dart';
import 'package:example/main.dart';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -60,6 +58,7 @@ class ChooseUserPage extends StatelessWidget {
(_, value) => value..extraData['image'] = getRandomPicUrl(value)); (_, value) => value..extraData['image'] = getRandomPicUrl(value));
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -73,6 +72,7 @@ class ChooseUserPage extends StatelessWidget {
child: SvgPicture.asset( child: SvgPicture.asset(
'assets/logo.svg', 'assets/logo.svg',
height: 40, height: 40,
color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
), ),
@@ -80,19 +80,12 @@ class ChooseUserPage extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 13.0), padding: const EdgeInsets.only(bottom: 13.0),
child: Text( child: Text(
'Welcome to Stream Chat', 'Welcome to Stream Chat',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.title,
fontSize: 22,
color: Colors.black,
fontWeight: FontWeight.bold,
),
), ),
), ),
Text( Text(
'Select a user to try the Flutter SDK:', 'Select a user to try the Flutter SDK:',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.body,
fontSize: 14.5,
color: Colors.black,
),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
@@ -100,9 +93,8 @@ class ChooseUserPage extends StatelessWidget {
child: ListView.separated( child: ListView.separated(
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
return Container( return Container(
width: double.infinity,
color: Colors.black12,
height: 1, height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
); );
}, },
itemCount: users.length + 1, itemCount: users.length + 1,
@@ -116,11 +108,16 @@ class ChooseUserPage extends StatelessWidget {
showDialog( showDialog(
barrierDismissible: false, barrierDismissible: false,
context: context, context: context,
barrierColor: StreamChatTheme.of(context)
.colorTheme
.overlay,
builder: (context) => Center( builder: (context) => Center(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
), ),
height: 100, height: 100,
width: 100, width: 100,
@@ -169,13 +166,24 @@ class ChooseUserPage extends StatelessWidget {
), ),
title: Text( title: Text(
user.name, user.name,
style: TextStyle(fontWeight: FontWeight.bold), style:
StreamChatTheme.of(context).textTheme.bodyBold,
), ),
subtitle: Text('Stream test account'), subtitle: Text(
trailing: SvgPicture.asset( 'Stream test account',
'assets/icon_arrow_right.svg', style: StreamChatTheme.of(context)
height: 24, .textTheme
width: 24, .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( leading: CircleAvatar(
child: StreamSvgIcon.settings( child: StreamSvgIcon.settings(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
backgroundColor: backgroundColor: StreamChatTheme.of(context)
StreamChatTheme.of(context).secondaryColor, .colorTheme
.greyWhisper,
), ),
title: Text( title: Text(
'Advanced Options', '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( trailing: SvgPicture.asset(
'assets/icon_arrow_right.svg', 'assets/icon_arrow_right.svg',
height: 24, height: 24,
+16 -14
View File
@@ -39,27 +39,29 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ThemeData( final themeData = ThemeData(primarySwatch: Colors.green);
primarySwatch: Colors.green, final defaultTheme = StreamChatThemeData.fromTheme(themeData);
); final customTheme = defaultTheme.merge(StreamChatThemeData(
return MaterialApp(
theme: theme,
builder: (context, child) => StreamChat(
child: child,
client: client,
streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageBackgroundColor: Colors.black, messageBackgroundColor: StreamChatTheme.of(context).colorTheme.black,
messageText: TextStyle( messageText: TextStyle(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
), ));
),
return MaterialApp(
theme: themeData,
builder: (context, child) {
return StreamChat(
child: child,
client: client,
streamChatThemeData: customTheme,
);
},
home: ChannelListPage(), home: ChannelListPage(),
); );
} }
+4 -2
View File
@@ -84,10 +84,12 @@ class ChannelListPage extends StatelessWidget {
channel: channel, channel: channel,
), ),
title: ChannelName( title: ChannelName(
channel: channel,
textStyle: textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title.copyWith( StreamChatTheme.of(context).channelPreviewTheme.title.copyWith(
color: Colors.black.withOpacity(opacity), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(opacity),
), ),
), ),
subtitle: Text(subtitle), subtitle: Text(subtitle),
@@ -127,7 +127,6 @@ class ChannelPage extends StatelessWidget {
showTimestamp: !details.isNextUser, showTimestamp: !details.isNextUser,
showUsername: !details.isNextUser, showUsername: !details.isNextUser,
showReactions: false, showReactions: false,
showReplyIndicator: false,
); );
} }
} }
+19 -15
View File
@@ -59,15 +59,16 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
return false; return false;
}, },
child: Scaffold( child: Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'Name of Group Chat', 'Name of Group Chat',
style: TextStyle( style: TextStyle(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -82,7 +83,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
'NAME', 'NAME',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context).colorTheme.grey,
), ),
), ),
SizedBox(width: 16), SizedBox(width: 16),
@@ -99,7 +100,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
hintText: 'Choose a group chat name', hintText: 'Choose a group chat name',
hintStyle: TextStyle( 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), padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.check( icon: StreamSvgIcon.check(
size: 24, size: 24,
color: _isGroupNameEmpty ? Colors.grey : Color(0xFF006CFF), color: _isGroupNameEmpty
? StreamChatTheme.of(context).colorTheme.grey
: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
onPressed: _isGroupNameEmpty onPressed: _isGroupNameEmpty
? null ? null
@@ -144,7 +149,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
children: [ children: [
Container( Container(
width: double.maxFinite, width: double.maxFinite,
color: Colors.grey.shade50, decoration: BoxDecoration(
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 8,
@@ -153,7 +160,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
child: Text( child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500, color: StreamChatTheme.of(context).colorTheme.grey,
), ),
), ),
), ),
@@ -166,17 +173,14 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
itemCount: _selectedUsers.length + 1, itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container( separatorBuilder: (_, __) => Container(
height: 1, height: 1,
color: Theme.of(context).brightness == Brightness.dark color: StreamChatTheme.of(context).colorTheme.greyWhisper,
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
), ),
itemBuilder: (_, index) { itemBuilder: (_, index) {
if (index == _selectedUsers.length) { if (index == _selectedUsers.length) {
return Container( return Container(
height: 1, height: 1,
color: Theme.of(context).brightness == Brightness.dark color:
? Colors.white.withOpacity(0.1) StreamChatTheme.of(context).colorTheme.greyWhisper,
: Colors.black.withOpacity(0.1),
); );
} }
final user = _selectedUsers[index]; final user = _selectedUsers[index];
@@ -200,7 +204,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
trailing: IconButton( trailing: IconButton(
icon: Icon( icon: Icon(
Icons.clear_rounded, Icons.clear_rounded,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
splashRadius: 24, splashRadius: 24,
File diff suppressed because it is too large Load Diff
+164 -22
View File
@@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:example/chat_info_screen.dart';
import 'package:example/choose_user_page.dart'; import 'package:example/choose_user_page.dart';
import 'package:example/new_chat_screen.dart'; import 'package:example/group_info_screen.dart';
import 'package:example/new_group_chat_screen.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -13,7 +14,6 @@ import 'notifications_service.dart';
import 'routes/app_routes.dart'; import 'routes/app_routes.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
import 'search_text_field.dart'; import 'search_text_field.dart';
import 'dart:async';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -27,7 +27,7 @@ void main() async {
logLevel: Level.INFO, logLevel: Level.INFO,
showLocalNotification: showLocalNotification:
(!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null,
persistenceEnabled: true, persistenceEnabled: false,
); );
if (userId != null) { if (userId != null) {
@@ -51,18 +51,20 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
return StreamChat( return StreamChat(
client: client, client: client,
child: MaterialApp( child: child,
);
},
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented themeMode: ThemeMode.system,
themeMode: ThemeMode.light,
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: initialRoute:
client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME,
),
); );
} }
} }
@@ -84,7 +86,9 @@ class _HomePageState extends State<HomePage> {
overflow: Overflow.visible, overflow: Overflow.visible,
children: [ children: [
StreamSvgIcon.message( StreamSvgIcon.message(
color: _isSelected(0) ? Colors.black : Colors.grey, color: _isSelected(0)
? StreamChatTheme.of(context).colorTheme.black
: Colors.grey,
), ),
Positioned( Positioned(
top: -3, top: -3,
@@ -100,7 +104,9 @@ class _HomePageState extends State<HomePage> {
overflow: Overflow.visible, overflow: Overflow.visible,
children: [ children: [
StreamSvgIcon.mentions( 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) { Widget build(BuildContext context) {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user;
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ChannelListHeader( appBar: ChannelListHeader(
onNewChatButtonTap: () { onNewChatButtonTap: () {
Navigator.pushNamed(context, Routes.NEW_CHAT); Navigator.pushNamed(context, Routes.NEW_CHAT);
@@ -121,10 +128,14 @@ class _HomePageState extends State<HomePage> {
drawer: _buildDrawer(context, user), drawer: _buildDrawer(context, user),
drawerEdgeDragWidth: 50, drawerEdgeDragWidth: 50,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
currentIndex: _currentIndex, currentIndex: _currentIndex,
items: _navBarItems, items: _navBarItems,
selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold,
unselectedLabelStyle:
StreamChatTheme.of(context).textTheme.footnoteBold,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.black, selectedItemColor: StreamChatTheme.of(context).colorTheme.black,
unselectedItemColor: Colors.grey, unselectedItemColor: Colors.grey,
onTap: (index) { onTap: (index) {
setState(() => _currentIndex = index); setState(() => _currentIndex = index);
@@ -142,6 +153,8 @@ class _HomePageState extends State<HomePage> {
Drawer _buildDrawer(BuildContext context, User user) { Drawer _buildDrawer(BuildContext context, User user) {
return Drawer( return Drawer(
child: Container(
color: StreamChatTheme.of(context).colorTheme.white,
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -176,7 +189,10 @@ class _HomePageState extends State<HomePage> {
), ),
ListTile( ListTile(
leading: StreamSvgIcon.penWrite( leading: StreamSvgIcon.penWrite(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
onTap: () { onTap: () {
Navigator.popAndPushNamed( Navigator.popAndPushNamed(
@@ -193,7 +209,10 @@ class _HomePageState extends State<HomePage> {
), ),
ListTile( ListTile(
leading: StreamSvgIcon.contacts( leading: StreamSvgIcon.contacts(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
onTap: () { onTap: () {
Navigator.popAndPushNamed( Navigator.popAndPushNamed(
@@ -228,7 +247,10 @@ class _HomePageState extends State<HomePage> {
); );
}, },
leading: StreamSvgIcon.user( leading: StreamSvgIcon.user(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
title: Text( title: Text(
'Sign out', 'Sign out',
@@ -243,6 +265,7 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
), ),
),
); );
} }
} }
@@ -467,6 +490,32 @@ class _ChannelListPageState extends State<ChannelListPage> {
limit: 20, limit: 20,
), ),
channelWidget: ChannelPage(), 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 int initialScrollIndex;
final double initialAlignment; final double initialAlignment;
final bool highlightInitialMessage; final bool highlightInitialMessage;
@@ -501,12 +550,75 @@ class ChannelPage extends StatelessWidget {
this.highlightInitialMessage = false, this.highlightInitialMessage = false,
}) : super(key: key); }) : 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ChannelHeader( appBar: ChannelHeader(
showTypingIndicator: false, 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( body: Column(
children: <Widget>[ children: <Widget>[
@@ -514,14 +626,35 @@ class ChannelPage extends StatelessWidget {
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MessageListView( MessageListView(
initialScrollIndex: initialScrollIndex, initialScrollIndex: widget.initialScrollIndex,
initialAlignment: initialAlignment, initialAlignment: widget.initialAlignment,
highlightInitialMessage: highlightInitialMessage, highlightInitialMessage: widget.highlightInitialMessage,
onMessageSwiped: _reply,
onReplyTap: _reply,
threadBuilder: (_, parentMessage) { threadBuilder: (_, parentMessage) {
return ThreadPage( return ThreadPage(
parent: parentMessage, 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( Positioned(
bottom: 0, bottom: 0,
@@ -529,7 +662,10 @@ class ChannelPage extends StatelessWidget {
right: 0, right: 0,
child: Container( child: Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
color: Color(0xffFCFCFC).withOpacity(.9), color: StreamChatTheme.of(context)
.colorTheme
.whiteSnow
.withOpacity(.9),
child: TypingIndicator( child: TypingIndicator(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric( 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ThreadHeader( appBar: ThreadHeader(
parent: parent, parent: parent,
), ),
+49 -38
View File
@@ -5,7 +5,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chips_input_text_field.dart'; import 'chips_input_text_field.dart';
import 'main.dart'; import 'main.dart';
import 'new_group_chat_screen.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
class NewChatScreen extends StatefulWidget { class NewChatScreen extends StatefulWidget {
@@ -37,8 +36,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
bool _showUserList = true; bool _showUserList = true;
bool _channelExisted = false;
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
@@ -122,17 +119,18 @@ class _NewChatScreenState extends State<NewChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 0, elevation: 0,
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'New Chat', 'New Chat',
style: TextStyle( style: StreamChatTheme.of(context)
color: Colors.black, .textTheme
fontSize: 16, .headlineBold
), .copyWith(color: StreamChatTheme.of(context).colorTheme.black),
), ),
centerTitle: true, centerTitle: true,
), ),
@@ -157,7 +155,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.05), color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.only(left: 24), padding: const EdgeInsets.only(left: 24),
@@ -165,12 +165,19 @@ class _NewChatScreenState extends State<NewChatScreen> {
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
child: Text( child: Text(
user.name, 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( child: UserAvatar(
showOnlineStatus: false, showOnlineStatus: false,
user: user, user: user,
@@ -180,11 +187,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
), ),
), ),
Positioned( StreamSvgIcon.close(),
child: StreamSvgIcon.close(
color: Colors.white,
),
),
], ],
), ),
); );
@@ -212,7 +215,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
StreamNeumorphicButton( StreamNeumorphicButton(
child: Center( child: Center(
child: StreamSvgIcon.contacts( child: StreamSvgIcon.contacts(
color: Color(0xFF006CFF), color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
size: 24, size: 24,
), ),
), ),
@@ -220,10 +225,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
SizedBox(width: 8), SizedBox(width: 8),
Text( Text(
'Create a Group', 'Create a Group',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.bodyBold,
fontWeight: FontWeight.bold,
fontSize: 16,
),
), ),
], ],
), ),
@@ -234,15 +236,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
Container( Container(
width: double.maxFinite, width: double.maxFinite,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withOpacity(0.02),
Colors.white.withOpacity(0.05),
],
stops: [0, 1],
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -253,10 +247,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
_isSearchActive _isSearchActive
? "Matches for \"$_userNameQuery\"" ? "Matches for \"$_userNameQuery\""
: 'On the platform', : 'On the platform',
style: TextStyle( style: StreamChatTheme.of(context)
color: Colors.black.withOpacity(0.5), .textTheme
), .footnote
), .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
), ),
), ),
Expanded( Expanded(
@@ -316,7 +314,17 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
), ),
Text( 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...', 'No chats here yet...',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
), ),
); );
+37 -35
View File
@@ -3,9 +3,8 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 'routes/routes.dart';
import 'search_text_field.dart';
class NewGroupChatScreen extends StatefulWidget { class NewGroupChatScreen extends StatefulWidget {
@override @override
@@ -51,23 +50,16 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return Scaffold(
child: Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
forceElevated: true,
elevation: 1, elevation: 1,
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'Add Group Members', 'Add Group Members',
style: TextStyle( style: TextStyle(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -76,7 +68,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
if (_selectedUsers.isNotEmpty) if (_selectedUsers.isNotEmpty)
IconButton( IconButton(
icon: StreamSvgIcon.arrow_right( icon: StreamSvgIcon.arrow_right(
color: Color(0xFF006CFF), color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
onPressed: () async { onPressed: () async {
final updatedList = await Navigator.pushNamed( final updatedList = await Navigator.pushNamed(
@@ -95,6 +87,10 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
) )
], ],
), ),
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter( SliverToBoxAdapter(
child: SearchTextField( child: SearchTextField(
controller: _controller, controller: _controller,
@@ -137,21 +133,24 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: Colors.grey.shade100, color: StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
), ),
), ),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
color: Colors.black, color: StreamChatTheme.of(context)
.colorTheme
.black,
size: 24, size: 24,
), ),
), ),
), ),
),
) )
], ],
), ),
@@ -176,15 +175,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
child: Container( child: Container(
width: double.maxFinite, width: double.maxFinite,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withOpacity(0.02),
Colors.white.withOpacity(0.05),
],
stops: [0, 1],
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -196,7 +187,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
? 'Matches for \"$_userNameQuery\"' ? 'Matches for \"$_userNameQuery\"'
: 'On the platform', : 'On the platform',
style: TextStyle( style: TextStyle(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context).colorTheme.grey,
), ),
), ),
), ),
@@ -258,10 +249,22 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search( child: StreamSvgIcon.search(
size: 96, size: 96,
color: Colors.grey, 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,
), ),
), ),
Text('No user matches these keywords...'),
], ],
), ),
), ),
@@ -274,7 +277,6 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
), ),
), ),
), ),
),
); );
} }
} }
@@ -292,7 +294,7 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate {
Widget build( Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) { BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container( return Container(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
child: child, child: child,
); );
} }
+16
View File
@@ -7,6 +7,8 @@ import '../main.dart';
import '../group_chat_details_screen.dart'; import '../group_chat_details_screen.dart';
import '../new_group_chat_screen.dart'; import '../new_group_chat_screen.dart';
import '../new_chat_screen.dart'; import '../new_chat_screen.dart';
import '../chat_info_screen.dart';
import '../group_info_screen.dart';
class AppRoutes { class AppRoutes {
/// Add entry for new route here /// Add entry for new route here
@@ -63,6 +65,20 @@ class AppRoutes {
selectedUsers: args, 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();
});
} }
} }
} }
+2
View File
@@ -7,4 +7,6 @@ class Routes {
static const String NEW_CHAT = '/new_chat'; static const String NEW_CHAT = '/new_chat';
static const String NEW_GROUP_CHAT = '/new_group_chat'; static const String NEW_GROUP_CHAT = '/new_group_chat';
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; 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';
} }
+9 -7
View File
@@ -22,9 +22,9 @@ class SearchTextField extends StatelessWidget {
return Container( return Container(
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
border: Border.all( border: Border.all(
color: Colors.grey.shade300, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
), ),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
), ),
@@ -40,6 +40,7 @@ class SearchTextField extends StatelessWidget {
controller: controller, controller: controller,
onChanged: onChanged, onChanged: onChanged,
decoration: InputDecoration( decoration: InputDecoration(
prefixText: ' ',
prefixIconConstraints: BoxConstraints.tight(Size(40, 24)), prefixIconConstraints: BoxConstraints.tight(Size(40, 24)),
prefixIcon: Padding( prefixIcon: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@@ -47,15 +48,16 @@ class SearchTextField extends StatelessWidget {
right: 8, right: 8,
), ),
child: StreamSvgIcon.search( child: StreamSvgIcon.search(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
size: 24, size: 24,
), ),
), ),
hintText: hintText, hintText: hintText,
hintStyle: TextStyle( hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
fontSize: 14, .colorTheme
), .black
.withOpacity(.5)),
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderSide: BorderSide.none,
+3 -2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamVersion extends StatelessWidget { class StreamVersion extends StatelessWidget {
const StreamVersion({ const StreamVersion({
@@ -27,8 +28,8 @@ class StreamVersion extends StatelessWidget {
return Text( return Text(
'Stream SDK v ${streamChatDep}', 'Stream SDK v ${streamChatDep}',
style: TextStyle( style: TextStyle(
fontSize: 14.5, fontSize: 14,
color: Colors.black.withOpacity(.13), color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
); );
}, },
+4 -3
View File
@@ -1,6 +1,6 @@
name: example name: example
description: A new Flutter project. description: A new Flutter project.
version: 1.0.94+97 version: 1.0.111+114
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.2.2 <3.0.0"
@@ -22,7 +22,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_driver: flutter_driver:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.8.0 flutter_launcher_icons: ^0.8.1
test: any test: any
flutter: flutter:
@@ -34,4 +34,5 @@ flutter:
flutter_icons: flutter_icons:
android: true android: true
ios: true ios: true
image_path: "assets/icon.png" image_path_android: "assets/android_icon.png"
image_path_ios: "assets/ios_icon.png"
+5 -3
View File
@@ -3,6 +3,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import '../stream_chat_flutter.dart';
class AttachmentError extends StatelessWidget { class AttachmentError extends StatelessWidget {
final Attachment attachment; final Attachment attachment;
final Size size; final Size size;
@@ -23,12 +25,12 @@ class AttachmentError extends StatelessWidget {
return Center( return Center(
child: Container( child: Container(
width: size?.width, width: size?.width,
height: size?.height, height: size?.height ?? 200,
color: Color(0xffd0021B).withOpacity(.1), color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
child: Center( child: Center(
child: Icon( child: Icon(
Icons.error_outline, Icons.error_outline,
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.black,
), ),
), ),
), ),
+1 -1
View File
@@ -32,7 +32,7 @@ class AttachmentTitle extends StatelessWidget {
attachment.title, attachment.title,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith( style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context).colorTheme.accentBlue,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
+3 -3
View File
@@ -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/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/src/unread_indicator.dart';
import '../stream_chat_flutter.dart';
class StreamBackButton extends StatelessWidget { class StreamBackButton extends StatelessWidget {
const StreamBackButton({ const StreamBackButton({
Key key, Key key,
@@ -35,9 +37,7 @@ class StreamBackButton extends StatelessWidget {
}, },
child: StreamSvgIcon.left( child: StreamSvgIcon.left(
size: 24, size: 24,
color: Theme.of(context).brightness == Brightness.dark color: StreamChatTheme.of(context).colorTheme.black,
? Colors.white
: Colors.black,
), ),
), ),
), ),
+172 -142
View File
@@ -1,193 +1,223 @@
import 'package:flutter/material.dart'; 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_info.dart';
import 'channel_name.dart'; import 'option_list_tile.dart';
import 'stream_chat.dart';
import 'stream_chat_theme.dart';
import 'user_avatar.dart';
class ChannelBottomSheet extends StatelessWidget { class ChannelBottomSheet extends StatefulWidget {
const ChannelBottomSheet({ final VoidCallback onViewInfoTap;
Key key,
}) : super(key: key); ChannelBottomSheet({this.onViewInfoTap});
@override
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
}
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
return SafeArea(
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),
),
),
child: ListView(
shrinkWrap: true,
children: [
SizedBox(
height: 24.0,
),
Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric(horizontal: 16.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( child: ChannelName(
textStyle: textStyle: StreamChatTheme.of(context).textTheme.headlineBold,
StreamChatTheme.of(context).channelPreviewTheme.title,
), ),
), ),
), ),
SizedBox(
height: 5.0,
), ),
Padding( Center(
padding: const EdgeInsets.symmetric(
vertical: 2.0,
),
child: Center(
child: ChannelInfo( child: ChannelInfo(
channel: channel, showTypingIndicator: false,
channel: StreamChannel.of(context).channel,
textStyle: textStyle:
StreamChatTheme.of(context).channelPreviewTheme.subtitle, StreamChatTheme.of(context).channelPreviewTheme.subtitle,
), ),
), ),
SizedBox(
height: 17.0,
), ),
Padding( if (channel.isDistinct && channel.memberCount == 2)
padding: const EdgeInsets.symmetric( Column(
vertical: 15.0, children: [
UserAvatar(
user: members
.firstWhere((e) => e.user.id != userAsMember.user.id)
.user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
), ),
child: Center( borderRadius: BorderRadius.circular(32.0),
child: StreamBuilder<List<Member>>( onlineIndicatorConstraints:
stream: channel.state.membersStream.map((event) => event BoxConstraints.tight(Size(16.0, 16.0)),
.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,
), ),
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,
), ),
Divider(), ],
if (channel.isGroup && !channel.isDistinct)
ListTile(
leading: StreamSvgIcon.userRemove(
size: 24,
color: Color(0xff7A7A7A),
), ),
title: Text( if (!(channel.isDistinct && channel.memberCount == 2))
'Leave Group', Container(
style: TextStyle(fontWeight: FontWeight.bold), 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,
), ),
onTap: () async { borderRadius: BorderRadius.circular(32.0),
final confirm = await showConfirmationDialog( ),
context, SizedBox(
title: 'Leave Group', height: 6.0,
okText: 'LEAVE', ),
question: 'Are you sure you want to leave this group?', Text(
cancelText: 'CANCEL', members[index].user.name,
icon: StreamSvgIcon.userRemove( style: StreamChatTheme.of(context)
color: Colors.red, .textTheme
.footnoteBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
), ),
); );
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( SizedBox(
'Delete chat', height: 24.0,
style: TextStyle( ),
color: Color(0xFFFF3742), 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 { 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( final res = await showConfirmationDialog(
context, context,
title: 'Delete Conversation', title: 'Delete Conversation',
okText: 'DELETE', okText: 'DELETE',
question: question: 'Are you sure you want to delete this conversation?',
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: Colors.red, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
); );
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
if (res == true) { if (res == true) {
await channel.delete().then((value) { await channel.delete();
Navigator.pop(context); Navigator.pop(context);
});
} }
},
),
],
),
),
);
} }
Widget _buildMembers( void _showLeaveDialog() async {
BuildContext context, final res = await showConfirmationDialog(
AsyncSnapshot<List<Member>> snapshot, context,
) { title: 'Leave conversation',
if (snapshot.data.isEmpty) { okText: 'LEAVE',
return SizedBox(); question: 'Are you sure you want to leave this conversation?',
} cancelText: 'CANCEL',
icon: StreamSvgIcon.userRemove(
return Container( color: StreamChatTheme.of(context).colorTheme.accentRed,
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,
),
child: Column(
children: <Widget>[
UserAvatar(
showOnlineStatus: true,
user: m.user,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tight(
Size.square(64),
),
),
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,
),
),
),
],
),
);
}).toList(),
), ),
); );
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
}
} }
} }
+182
View File
@@ -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,
);
}
}
+1 -28
View File
@@ -8,7 +8,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import './channel_name.dart'; import './channel_name.dart';
import 'channel_image.dart'; import 'channel_image.dart';
import 'chat_info_screen.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
@@ -99,33 +98,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
padding: const EdgeInsets.only(right: 10.0), padding: const EdgeInsets.only(right: 10.0),
child: Center( child: Center(
child: ChannelImage( child: ChannelImage(
onTap: onImageTap ?? 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);
}
}
}
},
), ),
), ),
), ),
+48 -8
View File
@@ -52,6 +52,9 @@ class ChannelImage extends StatelessWidget {
this.onTap, this.onTap,
this.showOnlineStatus = true, this.showOnlineStatus = true,
this.borderRadius, this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
final BorderRadius borderRadius; final BorderRadius borderRadius;
@@ -67,6 +70,12 @@ class ChannelImage extends StatelessWidget {
final bool showOnlineStatus; final bool showOnlineStatus;
final bool selected;
final Color selectionColor;
final double selectionThickness;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChat = StreamChat.of(context); final streamChat = StreamChat.of(context);
@@ -94,11 +103,11 @@ class ChannelImage extends StatelessWidget {
.channelPreviewTheme .channelPreviewTheme
.avatarTheme .avatarTheme
.constraints, .constraints,
onTap: onTap != null onTap: onTap != null ? (_) => onTap() : null,
? (_) { selected: selected,
onTap(); selectionColor: selectionColor ??
} StreamChatTheme.of(context).colorTheme.accentBlue,
: null, selectionThickness: selectionThickness,
); );
}); });
} else { } else {
@@ -111,16 +120,21 @@ class ChannelImage extends StatelessWidget {
.toList(); .toList();
return GroupImage( return GroupImage(
images: images, images: images,
borderRadius: borderRadius,
constraints: constraints ?? constraints: constraints ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
.avatarTheme .avatarTheme
.constraints, .constraints,
onTap: onTap, onTap: onTap,
selected: selected,
selectionColor: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
selectionThickness: selectionThickness,
); );
} }
return ClipRRect( Widget child = ClipRRect(
borderRadius: borderRadius ?? borderRadius: borderRadius ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
@@ -133,7 +147,7 @@ class ChannelImage extends StatelessWidget {
.avatarTheme .avatarTheme
.constraints, .constraints,
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
@@ -149,7 +163,9 @@ class ChannelImage extends StatelessWidget {
? snapshot.data['name'][0] ? snapshot.data['name'][0]
: '', : '',
style: TextStyle( style: TextStyle(
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold, 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;
}); });
} }
} }
+1 -1
View File
@@ -134,7 +134,7 @@ class ChannelInfo extends StatelessWidget {
child: Text( child: Text(
'Try Again', 'Try Again',
style: textStyle.copyWith( style: textStyle.copyWith(
color: Color(0xFF006CFF), color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
), ),
+4 -9
View File
@@ -99,7 +99,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
var color; var color;
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
color = Color(0xFF006CFF); color = StreamChatTheme.of(context).colorTheme.accentBlue;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
color = Colors.grey; color = Colors.grey;
@@ -144,13 +144,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget _buildConnectedTitleState(BuildContext context) => Text( Widget _buildConnectedTitleState(BuildContext context) => Text(
'Stream Chat', 'Stream Chat',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
.channelTheme color: StreamChatTheme.of(context).colorTheme.black,
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
), ),
); );
@@ -210,7 +205,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
.copyWith( .copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF006CFF), color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
), ),
+176 -52
View File
@@ -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/channels_bloc.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'dart:ui' as ui;
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'channel_bottom_sheet.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] /// Builder used to create a custom [ChannelPreview] from a [Channel]
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
typedef ViewInfoCallback = void Function(Channel);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png)
/// ///
@@ -74,6 +77,10 @@ class ChannelListView extends StatefulWidget {
this.onStartChatPressed, this.onStartChatPressed,
this.swipeToAction = false, this.swipeToAction = false,
this.pullToRefresh = true, this.pullToRefresh = true,
this.crossAxisCount = 1,
this.padding,
this.selectedChannels = const [],
this.onViewInfoTap,
}) : super(key: key); }) : super(key: key);
/// The builder that will be used in case of error /// 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 /// Callback used in the default empty list widget
final VoidCallback onStartChatPressed; 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 @override
_ChannelListViewState createState() => _ChannelListViewState(); _ChannelListViewState createState() => _ChannelListViewState();
} }
@@ -141,6 +158,7 @@ class ChannelListView extends StatefulWidget {
class _ChannelListViewState extends State<ChannelListView> class _ChannelListViewState extends State<ChannelListView>
with WidgetsBindingObserver { with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -203,16 +221,18 @@ class _ChannelListViewState extends State<ChannelListView>
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: StreamSvgIcon.message( child: StreamSvgIcon.message(
size: 136, size: 136,
color: Color(0xffDBDBDB), color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'Lets start chatting!', 'Lets start chatting!',
style: TextStyle( style: StreamChatTheme.of(context)
fontSize: 16, .textTheme
), .headline,
), ),
), ),
Padding( Padding(
@@ -223,9 +243,13 @@ class _ChannelListViewState extends State<ChannelListView>
child: Text( child: Text(
'How about sending your first message to a friend?', 'How about sending your first message to a friend?',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: StreamChatTheme.of(context)
fontSize: 14, .textTheme
color: Color(0xff7A7A7A), .body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
), ),
), ),
), ),
@@ -242,10 +266,13 @@ class _ChannelListViewState extends State<ChannelListView>
onPressed: widget.onStartChatPressed, onPressed: widget.onStartChatPressed,
child: Text( child: Text(
'Start a chat', 'Start a chat',
style: TextStyle( style: StreamChatTheme.of(context)
color: .textTheme
StreamChatTheme.of(context).accentColor, .bodyBold
fontWeight: FontWeight.bold, .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
), ),
), ),
), ),
@@ -259,22 +286,35 @@ class _ChannelListViewState extends State<ChannelListView>
} }
if (channels.isNotEmpty) { if (channels.isNotEmpty) {
child = ListView.custom( if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController, controller: _scrollController,
childrenDelegate: SliverChildBuilderDelegate( itemBuilder: (context, index) {
(context, i) { return _gridItemBuilder(context, index, channels);
return _itemBuilder(context, i, 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,16 +328,19 @@ class _ChannelListViewState extends State<ChannelListView>
Widget _buildLoadingWidget() { Widget _buildLoadingWidget() {
return ListView( return ListView(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
children: List.generate( children: List.generate(
25, 25,
(i) { (i) {
if (widget.crossAxisCount == 1) {
if (i % 2 != 0) { if (i % 2 != 0) {
if (widget.separatorBuilder != null) { if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i); return widget.separatorBuilder(context, i);
} }
return _separatorBuilder(context, i); return _separatorBuilder(context, i);
} }
}
return _buildLoadingItem(); return _buildLoadingItem();
}, },
), ),
@@ -305,13 +348,43 @@ class _ChannelListViewState extends State<ChannelListView>
} }
Shimmer _buildLoadingItem() { Shimmer _buildLoadingItem() {
if (widget.crossAxisCount > 1) {
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: Color(0xffE5E5E5), baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
highlightColor: Color(0xffffffff), 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,
),
],
),
);
} else {
return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: ListTile( child: ListTile(
leading: Container( leading: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -319,11 +392,15 @@ class _ChannelListViewState extends State<ChannelListView>
width: 40, width: 40,
), ),
), ),
contentPadding: const EdgeInsets.only(
left: 8,
right: 8,
),
title: Align( title: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -333,12 +410,13 @@ class _ChannelListViewState extends State<ChannelListView>
), ),
), ),
subtitle: Row( subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -350,7 +428,7 @@ class _ChannelListViewState extends State<ChannelListView>
Container( Container(
margin: const EdgeInsets.only(left: 16), margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -363,6 +441,7 @@ class _ChannelListViewState extends State<ChannelListView>
), ),
); );
} }
}
Widget _buildErrorWidget( Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot, AsyncSnapshot<List<Channel>> snapshot,
@@ -428,20 +507,10 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} }
Widget _itemBuilder(context, int i, List<Channel> channels) { Widget _listItemBuilder(BuildContext 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;
final channelsProvider = ChannelsBloc.of(context); final channelsProvider = ChannelsBloc.of(context);
if (i < channels.length) { if (i < channels.length) {
final channel = channels[i]; final channel = channels[i];
ChannelTapCallback onTap; ChannelTapCallback onTap;
if (widget.onChannelTap != null) { if (widget.onChannelTap != null) {
onTap = widget.onChannelTap; onTap = widget.onChannelTap;
@@ -493,10 +562,9 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} else { } else {
final backgroundColor = final backgroundColor =
Theme.of(context).brightness == Brightness.light StreamChatTheme.of(context).colorTheme.whiteSmoke;
? Color(0xffEBEBEB)
: Color(0xff141414);
child = Slidable( child = Slidable(
controller: _slideController,
enabled: widget.swipeToAction, enabled: widget.swipeToAction,
actionPane: SlidableBehindActionPane(), actionPane: SlidableBehindActionPane(),
actionExtentRatio: 0.12, actionExtentRatio: 0.12,
@@ -517,7 +585,11 @@ class _ChannelListViewState extends State<ChannelListView>
context: context, context: context,
builder: (context) { builder: (context) {
return StreamChannel( return StreamChannel(
child: ChannelBottomSheet(), child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap(channel);
},
),
channel: channel, channel: channel,
); );
}, },
@@ -535,7 +607,7 @@ class _ChannelListViewState extends State<ChannelListView>
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
iconWidget: StreamSvgIcon.delete( iconWidget: StreamSvgIcon.delete(
color: Color(0xFFFF3742), color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
onTap: () async { onTap: () async {
final res = await showConfirmationDialog( final res = await showConfirmationDialog(
@@ -546,7 +618,9 @@ class _ChannelListViewState extends State<ChannelListView>
'Are you sure you want to delete this conversation?', 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: Color(0xFFFF3742), color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
), ),
); );
if (res == true) { if (res == true) {
@@ -556,7 +630,7 @@ class _ChannelListViewState extends State<ChannelListView>
), ),
], ],
child: Container( child: Container(
color: StreamChatTheme.of(context).backgroundColor, color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: ChannelPreview( child: ChannelPreview(
onLongPress: widget.onChannelLongPress, onLongPress: widget.onChannelLongPress,
channel: channel, 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( Widget _buildQueryProgressIndicator(
context, context,
ChannelsBlocState channelsProvider, ChannelsBlocState channelsProvider,
@@ -591,7 +706,10 @@ class _ChannelListViewState extends State<ChannelListView>
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container( return Container(
color: Color(0xffd0021B).withAlpha(26), color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center( child: Center(
@@ -611,11 +729,17 @@ class _ChannelListViewState extends State<ChannelListView>
} }
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) {
return Container( var effect = StreamChatTheme.of(context).colorTheme.borderTop;
return BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: effect.sigmaX,
sigmaY: effect.sigmaY,
),
child: Container(
height: 1, height: 1,
color: Theme.of(context).brightness == Brightness.dark color: effect.color.withOpacity(0.08),
? Colors.white.withOpacity(0.1) ),
: Colors.black.withOpacity(0.1),
); );
} }
+231
View File
@@ -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);
}
+79 -38
View File
@@ -7,7 +7,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'channel_name.dart'; import 'channel_name.dart';
import 'channel_unread_indicator.dart'; import 'channel_unread_indicator.dart';
import 'chat_info_screen.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
@@ -50,6 +49,9 @@ class ChannelPreview extends StatelessWidget {
return Opacity( return Opacity(
opacity: snapshot.data ? 0.5 : 1, opacity: snapshot.data ? 0.5 : 1,
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: () { onTap: () {
if (onTap != null) { if (onTap != null) {
onTap(channel); onTap(channel);
@@ -61,29 +63,7 @@ class ChannelPreview extends StatelessWidget {
} }
}, },
leading: ChannelImage( leading: ChannelImage(
onTap: onImageTap ?? 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,
),
),
),
);
}
}
},
), ),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -122,12 +102,18 @@ class ChannelPreview extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator( child: SendingIndicator(
message: channel.state.lastMessage, message: channel.state.lastMessage,
allRead: channel.state.read size: StreamChatTheme.of(context)
.where((element) => element.lastRead .channelPreviewTheme
.indicatorIconSize,
isMessageRead: channel.state.read
?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(channel .isAfter(channel
.state.lastMessage.createdAt)) .state.lastMessage.createdAt))
.length == ?.isNotEmpty ==
(channel.memberCount ?? 0), true,
), ),
); );
} }
@@ -155,12 +141,18 @@ class ChannelPreview extends StatelessWidget {
String stringDate; String stringDate;
final now = DateTime.now(); final now = DateTime.now();
if (now.year != lastMessageAt.year || var startOfDay = DateTime(now.year, now.month, now.day);
now.month != lastMessageAt.month ||
now.day != lastMessageAt.day) { if (lastMessageAt.millisecondsSinceEpoch >=
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); startOfDay.millisecondsSinceEpoch) {
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); 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( return Text(
@@ -227,7 +219,9 @@ class ChannelPreview extends StatelessWidget {
} else if (e.type == 'giphy') { } else if (e.type == 'giphy') {
return '[GIF]'; return '[GIF]';
} }
return null; return e == lastMessage.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}).where((e) => e != null), }).where((e) => e != null),
lastMessage.text ?? '', lastMessage.text ?? '',
]; ];
@@ -235,11 +229,19 @@ class ChannelPreview extends StatelessWidget {
text = parts.join(' '); text = parts.join(' ');
} }
return Text( return Text.rich(
_getDisplayText(
text, text,
maxLines: 1, lastMessage.mentionedUsers,
overflow: TextOverflow.ellipsis, lastMessage.attachments,
style: 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( StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
@@ -248,9 +250,48 @@ class ChannelPreview extends StatelessWidget {
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic ? FontStyle.italic
: FontStyle.normal, : FontStyle.normal,
fontWeight: FontWeight.bold),
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
); );
}, },
); );
} }
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);
}
} }
+2 -1
View File
@@ -19,6 +19,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
if (!snapshot.hasData || snapshot.data == 0) { if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox(); return SizedBox();
} }
return Material( return Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
@@ -33,7 +34,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
), ),
child: Center( child: Center(
child: Text( child: Text(
'${snapshot.data}', '${snapshot.data > 99 ? '99+' : snapshot.data}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.white, color: Colors.white,
+4 -7
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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 /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { class DateDivider extends StatelessWidget {
@@ -39,19 +40,15 @@ class DateDivider extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: (Theme.of(context).brightness == Brightness.light color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
? Colors.black
: Colors.white)
.withOpacity(0.5),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
8, 8,
), ),
), ),
child: Text( child: Text(
dayInfo, dayInfo,
style: TextStyle( style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith(
fontSize: 12, color: StreamChatTheme.of(context).colorTheme.white,
color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
+10 -4
View File
@@ -41,8 +41,14 @@ class DeletedMessage extends StatelessWidget {
side: borderSide ?? side: borderSide ??
BorderSide( BorderSide(
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24) ? StreamChatTheme.of(context)
: Colors.black.withAlpha(24), .colorTheme
.white
.withAlpha(24)
: StreamChatTheme.of(context)
.colorTheme
.black
.withAlpha(24),
), ),
), ),
child: Padding( child: Padding(
@@ -58,8 +64,8 @@ class DeletedMessage extends StatelessWidget {
style: messageTheme.messageText.copyWith( style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: (Theme.of(context).brightness == Brightness.dark color: (Theme.of(context).brightness == Brightness.dark
? Colors.white ? StreamChatTheme.of(context).colorTheme.white
: Colors.black) : StreamChatTheme.of(context).colorTheme.black)
.withOpacity(.5), .withOpacity(.5),
), ),
), ),
+14
View File
@@ -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);
}
+15 -116
View File
@@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'media_utils.dart'; import 'media_utils.dart';
enum FileAttachmentType { local, online } enum FileAttachmentType { local, online }
@@ -61,13 +62,13 @@ class _FileAttachmentState extends State<FileAttachment> {
child: Container( child: Container(
width: widget.size?.width ?? 100, width: widget.size?.width ?? 100,
height: 56.0, height: 56.0,
margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: borderRadius:
widget.trailing != null ? BorderRadius.circular(16.0) : null, widget.trailing != null ? BorderRadius.circular(16.0) : null,
border: widget.trailing != null border: widget.trailing != null
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6))) ? Border.fromBorderSide(BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper))
: null, : null,
), ),
child: Row( child: Row(
@@ -88,10 +89,7 @@ class _FileAttachmentState extends State<FileAttachment> {
children: [ children: [
Text( Text(
widget.attachment?.title ?? 'File', widget.attachment?.title ?? 'File',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.bodyBold,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -99,11 +97,12 @@ class _FileAttachmentState extends State<FileAttachment> {
height: 3.0, height: 3.0,
), ),
Text( Text(
'${_getSizeText(widget.attachment.extraData['file_size'])}', '${getSizeText(widget.attachment.extraData['file_size'])}',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.body.copyWith(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
fontSize: 14.0, .colorTheme
), .black
.withOpacity(0.5)),
), ),
], ],
), ),
@@ -113,7 +112,7 @@ class _FileAttachmentState extends State<FileAttachment> {
widget.trailing ?? widget.trailing ??
IconButton( IconButton(
icon: StreamSvgIcon.cloud_download( icon: StreamSvgIcon.cloud_download(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () {
launchURL(context, widget.attachment.assetUrl); 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, width: 20.0,
height: 20.0, height: 20.0,
child: CircularProgressIndicator( child: CircularProgressIndicator(
backgroundColor: StreamChatTheme.of(context).accentColor, backgroundColor:
StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
); );
@@ -226,76 +195,6 @@ class _FileAttachmentState extends State<FileAttachment> {
break; break;
} }
} }
return getFileTypeImage(widget.attachment.extraData['mime_type']);
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';
}
} }
} }
+9
View File
@@ -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_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'stream_channel.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
typedef ShowMessageCallback = void Function(Message message, Channel channel);
/// A full screen image widget /// A full screen image widget
class FullScreenMedia extends StatefulWidget { class FullScreenMedia extends StatefulWidget {
/// The url of the image /// The url of the image
@@ -18,6 +21,7 @@ class FullScreenMedia extends StatefulWidget {
final int startIndex; final int startIndex;
final String userName; final String userName;
final DateTime sentAt; final DateTime sentAt;
final ShowMessageCallback onShowMessage;
/// Instantiate a new FullScreenImage /// Instantiate a new FullScreenImage
const FullScreenMedia({ const FullScreenMedia({
@@ -27,6 +31,7 @@ class FullScreenMedia extends StatefulWidget {
this.startIndex = 0, this.startIndex = 0,
this.userName = '', this.userName = '',
this.sentAt, this.sentAt,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -167,6 +172,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
message: widget.message, message: widget.message,
urls: widget.mediaAttachments, urls: widget.mediaAttachments,
currentIndex: _currentPage, currentIndex: _currentPage,
onShowMessage: () {
widget.onShowMessage(
widget.message, StreamChannel.of(context).channel);
},
), ),
ImageFooter( ImageFooter(
currentPage: _currentPage, currentPage: _currentPage,
+51 -17
View File
@@ -12,6 +12,7 @@ class GiphyAttachment extends StatelessWidget {
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Message message; final Message message;
final Size size; final Size size;
final ShowMessageCallback onShowMessage;
const GiphyAttachment({ const GiphyAttachment({
Key key, Key key,
@@ -19,6 +20,7 @@ class GiphyAttachment extends StatelessWidget {
this.messageTheme, this.messageTheme,
this.message, this.message,
this.size, this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -75,6 +77,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
})); }));
@@ -112,7 +115,10 @@ class GiphyAttachment extends StatelessWidget {
bottom: 16, bottom: 16,
left: 16, left: 16,
child: Material( child: Material(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -124,13 +130,16 @@ class GiphyAttachment extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.lightning( StreamSvgIcon.lightning(
color: Colors.white, color:
StreamChatTheme.of(context).colorTheme.white,
size: 16, size: 16,
), ),
Text( Text(
'GIPHY', 'GIPHY',
style: TextStyle( style: TextStyle(
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11, fontSize: 11,
), ),
@@ -202,7 +211,10 @@ class GiphyAttachment extends StatelessWidget {
height: 4.0, height: 4.0,
), ),
Container( Container(
color: Colors.black.withOpacity(0.2), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
width: double.infinity, width: double.infinity,
height: 0.5, height: 0.5,
), ),
@@ -220,15 +232,24 @@ class GiphyAttachment extends StatelessWidget {
}, },
child: Text( child: Text(
'Cancel', 'Cancel',
style: TextStyle( style: StreamChatTheme.of(context)
fontWeight: FontWeight.bold, .textTheme
color: Colors.black.withOpacity(0.5)), .bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
), ),
), ),
), ),
Container( Container(
width: 0.5, width: 0.5,
color: Colors.black.withOpacity(0.2), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
height: 50.0, height: 50.0,
), ),
Expanded( Expanded(
@@ -242,7 +263,9 @@ class GiphyAttachment extends StatelessWidget {
child: Text( child: Text(
'Send', 'Send',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
), ),
@@ -263,7 +286,10 @@ class GiphyAttachment extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
StreamSvgIcon.eye( StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 16.0, size: 16.0,
), ),
SizedBox( SizedBox(
@@ -271,10 +297,14 @@ class GiphyAttachment extends StatelessWidget {
), ),
Text( Text(
'Only visible to you', 'Only visible to you',
style: TextStyle( style: StreamChatTheme.of(context)
color: Colors.black.withOpacity(0.5), .textTheme
fontSize: 12.0, .footnote
), .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
), ),
], ],
), ),
@@ -300,6 +330,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
})); }));
@@ -331,7 +362,10 @@ class GiphyAttachment extends StatelessWidget {
bottom: 8, bottom: 8,
left: 8, left: 8,
child: Material( child: Material(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -343,13 +377,13 @@ class GiphyAttachment extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.lightning( StreamSvgIcon.lightning(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
size: 16, size: 16,
), ),
Text( Text(
'GIPHY', 'GIPHY',
style: TextStyle( style: TextStyle(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11, fontSize: 11,
), ),
+35 -3
View File
@@ -9,18 +9,30 @@ class GroupImage extends StatelessWidget {
@required this.images, @required this.images,
this.constraints, this.constraints,
this.onTap, this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
final List<String> images; final List<String> images;
final BoxConstraints constraints; final BoxConstraints constraints;
final VoidCallback onTap; final VoidCallback onTap;
final bool selected;
final BorderRadius borderRadius;
final Color selectionColor;
final double selectionThickness;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( var avatar;
final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector(
onTap: onTap, onTap: onTap,
child: ClipRRect( child: ClipRRect(
borderRadius: StreamChatTheme.of(context) borderRadius: borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme .ownMessageTheme
.avatarTheme .avatarTheme
.borderRadius, .borderRadius,
@@ -31,7 +43,7 @@ class GroupImage extends StatelessWidget {
.avatarTheme .avatarTheme
.constraints, .constraints,
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
child: Flex( child: Flex(
direction: Axis.vertical, 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;
} }
} }
+35 -75
View File
@@ -1,48 +1,36 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import '../stream_chat_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../stream_chat_flutter.dart';
class ImageActionsModal extends StatelessWidget { class ImageActionsModal extends StatelessWidget {
final Message message; final Message message;
final String userName; final String userName;
final String sentAt; final String sentAt;
final List<Attachment> urls; final List<Attachment> urls;
final currentIndex; final currentIndex;
final VoidCallback onShowMessage;
ImageActionsModal( ImageActionsModal(
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex}); {this.message,
this.userName,
this.sentAt,
this.urls,
this.currentIndex,
this.onShowMessage});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () => Navigator.maybePop(context),
Navigator.pop(context); child: _buildPage(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),
],
),
); );
} }
@@ -50,49 +38,15 @@ class ImageActionsModal extends StatelessWidget {
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Row( SizedBox(
mainAxisAlignment: MainAxisAlignment.spaceBetween, height: kToolbarHeight,
children: [ child: IconButton(
Padding( icon: StreamSvgIcon.close(),
padding: const EdgeInsets.all(8.0), onPressed: () => Navigator.maybePop(context),
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);
},
),
],
),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
@@ -104,10 +58,10 @@ class ImageActionsModal extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
color: Color(0xffe5e5e5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: ListTile.divideTiles( children: ListTile.divideTiles(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
context: context, context: context,
tiles: [ tiles: [
_buildButton( _buildButton(
@@ -115,7 +69,8 @@ class ImageActionsModal extends StatelessWidget {
'Reply', 'Reply',
StreamSvgIcon.Icon_curve_line_left_up( StreamSvgIcon.Icon_curve_line_left_up(
size: 24.0, size: 24.0,
color: Colors.black.withOpacity(0.5), color:
StreamChatTheme.of(context).colorTheme.grey,
), ),
() {}), () {}),
_buildButton( _buildButton(
@@ -123,17 +78,17 @@ class ImageActionsModal extends StatelessWidget {
'Show in Chat', 'Show in Chat',
StreamSvgIcon.eye( StreamSvgIcon.eye(
size: 24.0, size: 24.0,
color: Colors.black, color:
), () { StreamChatTheme.of(context).colorTheme.black,
Navigator.pop(context); ),
Navigator.pop(context); onShowMessage),
}),
_buildButton( _buildButton(
context, context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}', 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.Icon_save( StreamSvgIcon.Icon_save(
size: 24.0, size: 24.0,
color: Colors.black.withOpacity(0.5), color:
StreamChatTheme.of(context).colorTheme.grey,
), () async { ), () async {
var url = urls[currentIndex].imageUrl ?? var url = urls[currentIndex].imageUrl ??
urls[currentIndex].assetUrl ?? urls[currentIndex].assetUrl ??
@@ -153,7 +108,9 @@ class ImageActionsModal extends StatelessWidget {
'Delete', 'Delete',
StreamSvgIcon.delete( StreamSvgIcon.delete(
size: 24.0, size: 24.0,
color: Color(0xffFF3742), color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
), ),
() { () {
Navigator.pop(context); Navigator.pop(context);
@@ -163,7 +120,9 @@ class ImageActionsModal extends StatelessWidget {
StreamChannel.of(context).channel.cid, StreamChannel.of(context).channel.cid,
); );
}, },
color: Color(0xffFF3742), color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
), ),
], ],
).toList(), ).toList(),
@@ -182,10 +141,11 @@ class ImageActionsModal extends StatelessWidget {
{Color color}) { {Color color}) {
var titleStyle = TextStyle( var titleStyle = TextStyle(
fontSize: 14.5, fontSize: 14.5,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
); );
return Material( return Material(
color: StreamChatTheme.of(context).colorTheme.white,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: ListTile( child: ListTile(
+8 -2
View File
@@ -12,6 +12,8 @@ class ImageAttachment extends StatelessWidget {
final Message message; final Message message;
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Size size; final Size size;
final bool showTitle;
final ShowMessageCallback onShowMessage;
const ImageAttachment({ const ImageAttachment({
Key key, Key key,
@@ -19,6 +21,8 @@ class ImageAttachment extends StatelessWidget {
@required this.message, @required this.message,
@required this.size, @required this.size,
this.messageTheme, this.messageTheme,
this.showTitle = true,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -54,6 +58,7 @@ class ImageAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
}, },
@@ -83,7 +88,7 @@ class ImageAttachment extends StatelessWidget {
), ),
), ),
), ),
if (attachment.title != null) if (showTitle && attachment.title != null)
Material( Material(
color: messageTheme.messageBackgroundColor, color: messageTheme.messageBackgroundColor,
child: AttachmentTitle( 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( Positioned.fill(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
+87 -347
View File
@@ -5,20 +5,17 @@ import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:dio/dio.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart'; import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/stream_chat.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/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_channel.dart'; class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
class ImageFooter extends StatefulWidget {
/// Callback to call when pressing the back button. /// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
final VoidCallback onBackPressed; final VoidCallback onBackPressed;
@@ -63,12 +60,12 @@ class ImageFooter extends StatefulWidget {
class _ImageFooterState extends State<ImageFooter> { class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false; bool _userSearchMode = false;
TextEditingController _searchController; TextEditingController _searchController;
TextEditingController _messageController = TextEditingController(); final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
String _userNameQuery; String _channelNameQuery;
bool _isSearchActive = false;
Set<User> _selectedUsers = {}; final List<Channel> _selectedChannels = [];
bool _loading = false; bool _loading = false;
Timer _debounce; Timer _debounce;
@@ -80,8 +77,7 @@ class _ImageFooterState extends State<ImageFooter> {
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) { if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() { modalSetStateCallback(() {
_userNameQuery = _searchController.text; _channelNameQuery = _searchController.text;
_isSearchActive = _userNameQuery.isNotEmpty;
}); });
} }
}); });
@@ -91,6 +87,9 @@ class _ImageFooterState extends State<ImageFooter> {
void initState() { void initState() {
super.initState(); super.initState();
_searchController = TextEditingController()..addListener(_userNameListener); _searchController = TextEditingController()..addListener(_userNameListener);
_messageFocusNode.addListener(() {
setState(() {});
});
} }
@override @override
@@ -103,21 +102,38 @@ class _ImageFooterState extends State<ImageFooter> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SizedBox.fromSize(
child: Container( size: Size(
color: MediaQuery.of(context).size.width,
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, MediaQuery.of(context).padding.bottom + widget.preferredSize.height,
padding: EdgeInsets.symmetric(vertical: 8.0), ),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: BottomAppBar(
color: StreamChatTheme.of(context).colorTheme.white,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( IconButton(
icon: StreamSvgIcon.icon_SHARE( icon: StreamSvgIcon.icon_SHARE(
size: 24.0, size: 24.0,
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () async {
_buildShareModal(context); 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');
}, },
), ),
InkWell( InkWell(
@@ -130,11 +146,8 @@ class _ImageFooterState extends State<ImageFooter> {
children: <Widget>[ children: <Widget>[
Text( Text(
'${widget.currentPage + 1} of ${widget.totalPages}', '${widget.currentPage + 1} of ${widget.totalPages}',
style: TextStyle( style:
fontSize: 16.0, StreamChatTheme.of(context).textTheme.headlineBold,
fontWeight: FontWeight.w700,
color: Colors.black,
),
), ),
], ],
), ),
@@ -142,25 +155,25 @@ class _ImageFooterState extends State<ImageFooter> {
), ),
IconButton( IconButton(
icon: StreamSvgIcon.Icon_grid( icon: StreamSvgIcon.Icon_grid(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () => _showPhotosModal(context),
_buildPhotosModal(context);
},
), ),
], ],
), ),
), ),
),
); );
} }
Widget _buildPhotosModal(context) { void _showPhotosModal(context) {
var videoAttachments = widget.mediaAttachments var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video') .where((element) => element.type == 'video')
.toList(); .toList();
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
), ),
@@ -170,7 +183,7 @@ class _ImageFooterState extends State<ImageFooter> {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0), topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16.0),
@@ -182,11 +195,8 @@ class _ImageFooterState extends State<ImageFooter> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Text(
'Photos', 'Photos',
style: TextStyle( style:
fontSize: 16.0, StreamChatTheme.of(context).textTheme.headlineBold,
fontWeight: FontWeight.w700,
color: Colors.black,
),
), ),
), ),
), ),
@@ -194,18 +204,16 @@ class _ImageFooterState extends State<ImageFooter> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: IconButton( child: IconButton(
icon: StreamSvgIcon.close( icon: StreamSvgIcon.close(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () => Navigator.maybePop(context),
Navigator.pop(context);
},
), ),
), ),
], ],
), ),
), ),
Container( Container(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
child: GridView.builder( child: GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
@@ -258,278 +266,16 @@ 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) { Widget _buildShareTextInputSection(modalSetState) {
return Align( return Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: BottomAppBar(
child: Container( child: Container(
color: Colors.white, height: 40.0,
height: 56.0, margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: _loading child: _loading
? Center( ? Center(
child: Padding( child: Padding(
@@ -540,10 +286,9 @@ class _ImageFooterState extends State<ImageFooter> {
: Row( : Row(
children: [ children: [
Expanded( Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: TextField( child: TextField(
controller: _messageController, controller: _messageController,
focusNode: _messageFocusNode,
onChanged: (val) { onChanged: (val) {
modalSetState(() {}); modalSetState(() {});
}, },
@@ -552,48 +297,53 @@ class _ImageFooterState extends State<ImageFooter> {
setState(() {}); setState(() {});
}, },
decoration: InputDecoration( decoration: InputDecoration(
isDense: true,
prefixText: ' ', prefixText: ' ',
hintText: 'Add a comment', hintText: 'Add a comment',
border: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.black.withOpacity(0.16), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.16),
), ),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide( 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),
),
), ),
), ),
), ),
SizedBox(width: 8),
IconTheme( IconTheme(
data: StreamChatTheme.of(context) data: StreamChatTheme.of(context)
.channelTheme .channelTheme
.messageInputButtonIconTheme, .messageInputButtonIconTheme,
child: Center( child: IconButton(
child: Padding( onPressed: () async {
padding: const EdgeInsets.all(8.0), modalSetState(() => _loading = true);
child: InkWell(
onTap: () async {
modalSetState(() {
_loading = true;
});
await sendMessage(); await sendMessage();
modalSetState(() { modalSetState(() => _loading = false);
_loading = false;
});
}, },
child: Transform.rotate( splashRadius: 24,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
padding: EdgeInsets.zero,
icon: Transform.rotate(
angle: -pi / 2, angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message( child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context)
), .colorTheme
), .accentBlue,
), ),
), ),
), ),
@@ -601,6 +351,7 @@ class _ImageFooterState extends State<ImageFooter> {
], ],
), ),
), ),
),
); );
} }
@@ -612,27 +363,16 @@ class _ImageFooterState extends State<ImageFooter> {
_messageController.clear(); _messageController.clear();
final client = StreamChat.of(context).client; for (var channel in _selectedChannels) {
for (var user in _selectedUsers) {
var c = client.channel('messaging', extraData: {
'members': [
user.id,
StreamChat.of(context).user.id,
],
});
await c.watch();
final message = Message( final message = Message(
text: text, text: text,
attachments: [attachments[widget.currentPage]], attachments: [attachments[widget.currentPage]],
); );
await c.sendMessage(message); await channel.sendMessage(message);
} }
_selectedUsers.clear(); _selectedChannels.clear();
Navigator.pop(context); Navigator.pop(context);
} }
+5 -4
View File
@@ -1,6 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart'; 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:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart';
@@ -12,11 +10,13 @@ class ImageGroup extends StatelessWidget {
@required this.images, @required this.images,
@required this.message, @required this.message,
@required this.size, @required this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
final List<Attachment> images; final List<Attachment> images;
final Message message; final Message message;
final Size size; final Size size;
final ShowMessageCallback onShowMessage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -81,10 +81,10 @@ class ImageGroup extends StatelessWidget {
color: Colors.black38, color: Colors.black38,
child: Center( child: Center(
child: Text( child: Text(
'${images.length - 4} +', '+ ${images.length - 4}',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 28, fontSize: 26,
), ),
), ),
), ),
@@ -121,6 +121,7 @@ class ImageGroup extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
), ),
), ),
+9 -7
View File
@@ -13,6 +13,9 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
final VoidCallback onBackPressed; final VoidCallback onBackPressed;
/// Callback to call when pressing the show message button.
final VoidCallback onShowMessage;
/// Callback to call when the header is tapped. /// Callback to call when the header is tapped.
final VoidCallback onTitleTap; final VoidCallback onTitleTap;
@@ -35,6 +38,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
this.currentIndex, this.currentIndex,
this.showBackButton = true, this.showBackButton = true,
this.onBackPressed, this.onBackPressed,
this.onShowMessage,
this.onTitleTap, this.onTitleTap,
this.onImageTap, this.onImageTap,
this.userName = '', this.userName = '',
@@ -50,7 +54,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
leading: showBackButton leading: showBackButton
? IconButton( ? IconButton(
icon: StreamSvgIcon.close( icon: StreamSvgIcon.close(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0, size: 24.0,
), ),
onPressed: onBackPressed, onPressed: onBackPressed,
@@ -61,7 +65,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: StreamSvgIcon.Icon_menu_point_v( icon: StreamSvgIcon.Icon_menu_point_v(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () {
_showMessageActionModalBottomSheet(context); _showMessageActionModalBottomSheet(context);
@@ -81,11 +85,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
children: <Widget>[ children: <Widget>[
Text( Text(
userName, userName,
style: TextStyle( style: StreamChatTheme.of(context).textTheme.headlineBold,
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
), ),
Text( Text(
sentAt, sentAt,
@@ -106,6 +106,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) {
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
@@ -115,6 +116,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
message: message, message: message,
urls: urls, urls: urls,
currentIndex: currentIndex, currentIndex: currentIndex,
onShowMessage: onShowMessage,
), ),
); );
}); });
+1 -1
View File
@@ -31,7 +31,7 @@ class LazyLoadScrollView extends StatefulWidget {
final bool isLoading; final bool isLoading;
/// Initiates a LazyLoadScrollView widget /// Initiates a LazyLoadScrollView widget
LazyLoadScrollView({ const LazyLoadScrollView({
Key key, Key key,
@required this.child, @required this.child,
this.onStartOfPage, this.onStartOfPage,
+25 -15
View File
@@ -1,10 +1,13 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'dart:ui' as ui;
import '../stream_chat_flutter.dart';
extension on Duration { extension on Duration {
String format() { String format() {
@@ -26,6 +29,7 @@ class MediaListView extends StatefulWidget {
this.selectedIds = const [], this.selectedIds = const [],
this.onSelect, this.onSelect,
}) : super(key: key); }) : super(key: key);
@override @override
_MediaListViewState createState() => _MediaListViewState(); _MediaListViewState createState() => _MediaListViewState();
} }
@@ -34,14 +38,11 @@ class _MediaListViewState extends State<MediaListView> {
final _media = <AssetEntity>[]; final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _currentPage = 0; int _currentPage = 0;
bool _endPagination = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LazyLoadScrollView( return LazyLoadScrollView(
onEndOfPage: () { onEndOfPage: () async => _getMedia(),
_getMedia();
},
child: GridView.builder( child: GridView.builder(
itemCount: _media.length, itemCount: _media.length,
controller: _scrollController, controller: _scrollController,
@@ -80,7 +81,10 @@ class _MediaListViewState extends State<MediaListView> {
? 1.0 ? 1.0
: 0.0, : 0.0,
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
alignment: Alignment.topRight, alignment: Alignment.topRight,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 8, top: 8,
@@ -88,10 +92,12 @@ class _MediaListViewState extends State<MediaListView> {
), ),
child: CircleAvatar( child: CircleAvatar(
radius: 12, radius: 12,
backgroundColor: Colors.white, backgroundColor:
StreamChatTheme.of(context).colorTheme.white,
child: StreamSvgIcon.check( child: StreamSvgIcon.check(
size: 24, size: 24,
color: Colors.black, color:
StreamChatTheme.of(context).colorTheme.black,
), ),
), ),
), ),
@@ -113,7 +119,7 @@ class _MediaListViewState extends State<MediaListView> {
child: Text( child: Text(
media.videoDuration.format(), media.videoDuration.format(),
style: TextStyle( style: TextStyle(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
), ),
), ),
@@ -141,15 +147,19 @@ class _MediaListViewState extends State<MediaListView> {
void _getMedia() async { void _getMedia() async {
final assetList = await PhotoManager.getAssetPathList( final assetList = await PhotoManager.getAssetPathList(
hasAll: true, 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); final media = await assetList.getAssetListPaged(_currentPage, 50);
if (media.isEmpty) { if (!media.isEmpty) {
setState(() {
_endPagination = true;
});
} else {
setState(() { setState(() {
_media.addAll(media); _media.addAll(media);
}); });
+104 -46
View File
@@ -15,14 +15,17 @@ import 'stream_chat_theme.dart';
class MessageActionsModal extends StatelessWidget { class MessageActionsModal extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder; 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 Message message;
final MessageTheme messageTheme; final MessageTheme messageTheme;
final bool showReactions; final bool showReactions;
final bool showDeleteMessage; final bool showDeleteMessage;
final bool showCopyMessage; final bool showCopyMessage;
final bool showEditMessage; final bool showEditMessage;
final bool showResendMessage;
final bool showReply; final bool showReply;
final bool showThreadReply;
final bool reverse; final bool reverse;
final ShapeBorder messageShape; final ShapeBorder messageShape;
final DisplayWidget showUserAvatar; final DisplayWidget showUserAvatar;
@@ -34,9 +37,12 @@ class MessageActionsModal extends StatelessWidget {
this.showReactions = true, this.showReactions = true,
this.showDeleteMessage = true, this.showDeleteMessage = true,
this.showEditMessage = true, this.showEditMessage = true,
this.onThreadTap, this.onReplyTap,
this.onThreadReplyTap,
this.showCopyMessage = true, this.showCopyMessage = true,
this.showReply = true, this.showReply = true,
this.showResendMessage = true,
this.showThreadReply = true,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.messageShape, this.messageShape,
@@ -49,27 +55,35 @@ class MessageActionsModal extends StatelessWidget {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user;
final roughMaxSize = 2 * size.width / 3; 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 = final roughSentenceSize =
message.text.length * messageTheme.messageText.fontSize * 1.2; messageTextLength * messageTheme.messageText.fontSize * 1.2;
final divFactor = message.attachments?.isNotEmpty == true final divFactor = message.attachments?.isNotEmpty == true
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () => Navigator.maybePop(context),
Navigator.pop(context);
},
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: 10.8731, sigmaX: 10,
sigmaY: 10.8731, sigmaY: 10,
), ),
child: Container( child: Container(
color: Colors.black.withOpacity(0.1), color: StreamChatTheme.of(context).colorTheme.overlay,
), ),
), ),
), ),
@@ -114,10 +128,14 @@ class MessageActionsModal extends StatelessWidget {
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showReplyIndicator: false,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showReactionPickerIndicator: true, showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
showInChannelIndicator: false, showInChannelIndicator: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
shape: messageShape, shape: messageShape,
@@ -125,9 +143,7 @@ class MessageActionsModal extends StatelessWidget {
), ),
); );
}), }),
SizedBox( SizedBox(height: 8),
height: 8,
),
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
@@ -143,6 +159,9 @@ class MessageActionsModal extends StatelessWidget {
horizontal: 48.0, horizontal: 48.0,
), ),
child: Material( child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -159,6 +178,14 @@ class MessageActionsModal extends StatelessWidget {
message.status == null) && message.status == null) &&
message.parentId == null) message.parentId == null)
_buildReplyButton(context), _buildReplyButton(context),
if (showThreadReply &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null) &&
message.parentId == null)
_buildThreadReplyButton(context),
if (showResendMessage)
_buildResendMessage(context),
if (showEditMessage) if (showEditMessage)
_buildEditMessage(context), _buildEditMessage(context),
if (showDeleteMessage) if (showDeleteMessage)
@@ -182,10 +209,29 @@ class MessageActionsModal extends StatelessWidget {
); );
} }
Widget _buildDeleteButton(BuildContext context) { Widget _buildReplyButton(BuildContext context) {
return ListTile( return ListTile(
title: Text( 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: style:
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red), 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) { void _showEditBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showModalBottomSheet( showModalBottomSheet(
@@ -241,10 +310,11 @@ class MessageActionsModal extends StatelessWidget {
elevation: 2, elevation: 2,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(32), topLeft: Radius.circular(16),
topRight: Radius.circular(32), topRight: Radius.circular(16),
), ),
), ),
builder: (context) { builder: (context) {
@@ -256,39 +326,26 @@ class MessageActionsModal extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
IconButton( Padding(
icon: StreamSvgIcon.edit( padding: const EdgeInsets.all(8.0),
size: 22, child: StreamSvgIcon.edit(
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).primaryIconTheme.color, .colorTheme
.greyGainsboro,
), ),
onPressed: () {},
), ),
Text( Text(
'Edit message', 'Edit Message',
style: Theme.of(context) style: TextStyle(fontWeight: FontWeight.bold),
.textTheme
.headline6
.copyWith(fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
icon: Icon( visualDensity: VisualDensity.compact,
Icons.cancel_outlined, icon: StreamSvgIcon.close_small(),
size: 22, onPressed: Navigator.of(context).pop,
color:
StreamChatTheme.of(context).primaryIconTheme.color,
),
onPressed: () {
Navigator.of(context).pop();
},
), ),
], ],
), ),
@@ -301,9 +358,10 @@ class MessageActionsModal extends StatelessWidget {
? editMessageInputBuilder(context, message) ? editMessageInputBuilder(context, message)
: MessageInput( : MessageInput(
editMessage: message, editMessage: message,
onMessageSent: (_) { preMessageSending: (m) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
Navigator.pop(context); Navigator.pop(context);
return m;
}, },
), ),
), ),
@@ -314,7 +372,7 @@ class MessageActionsModal extends StatelessWidget {
); );
} }
Widget _buildReplyButton(BuildContext context) { Widget _buildThreadReplyButton(BuildContext context) {
return ListTile( return ListTile(
title: Text( title: Text(
'Thread reply', 'Thread reply',
@@ -325,8 +383,8 @@ class MessageActionsModal extends StatelessWidget {
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (onThreadTap != null) { if (onThreadReplyTap != null) {
onThreadTap(message); onThreadReplyTap(message);
} }
}, },
); );
+227 -132
View File
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:emojis/emoji.dart'; import 'package:emojis/emoji.dart';
import 'package:file_picker/file_picker.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:stream_chat_flutter/src/user_avatar.dart';
import 'package:substring_highlight/substring_highlight.dart'; import 'package:substring_highlight/substring_highlight.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import 'extension.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'quoted_message_widget.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
typedef FileUploader = Future<String> Function(PlatformFile, Channel); typedef FileUploader = Future<String> Function(PlatformFile, Channel);
@@ -107,6 +110,8 @@ class MessageInput extends StatefulWidget {
this.actionsLocation = ActionsLocation.left, this.actionsLocation = ActionsLocation.left,
this.attachmentThumbnailBuilders, this.attachmentThumbnailBuilders,
this.focusNode, this.focusNode,
this.quotedMessage,
this.onQuotedMessageCleared,
}) : super(key: key); }) : super(key: key);
/// Message to edit /// Message to edit
@@ -155,6 +160,12 @@ class MessageInput extends StatefulWidget {
/// The focus node associated to the TextField /// The focus node associated to the TextField
final FocusNode focusNode; final FocusNode focusNode;
///
final Message quotedMessage;
///
final VoidCallback onQuotedMessageCleared;
@override @override
MessageInputState createState() => MessageInputState(); MessageInputState createState() => MessageInputState();
@@ -166,7 +177,7 @@ class MessageInput extends StatefulWidget {
if (messageInputState == null) { if (messageInputState == null) {
throw Exception( 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; return messageInputState;
@@ -196,9 +207,11 @@ class MessageInputState extends State<MessageInput> {
/// The editing controller passed to the input TextField /// The editing controller passed to the input TextField
TextEditingController textEditingController; TextEditingController textEditingController;
bool get _hasQuotedMessage => widget.quotedMessage != null;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( Widget child = SafeArea(
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
if (details.delta.dy > 0) { if (details.delta.dy > 0) {
@@ -213,6 +226,32 @@ class MessageInputState extends State<MessageInput> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: _buildTextField(context), 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) { Flex _buildTextField(BuildContext context) {
@@ -263,7 +310,10 @@ class MessageInputState extends State<MessageInput> {
border: _sendAsDm border: _sendAsDm
? null ? null
: Border.all( : Border.all(
color: Colors.black.withOpacity(.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
@@ -272,8 +322,8 @@ class MessageInputState extends State<MessageInput> {
child: Material( child: Material(
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
color: _sendAsDm color: _sendAsDm
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.white, : StreamChatTheme.of(context).colorTheme.white,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -288,7 +338,7 @@ class MessageInputState extends State<MessageInput> {
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: StreamSvgIcon.check( firstChild: StreamSvgIcon.check(
size: 16.0, size: 16.0,
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
secondChild: SizedBox( secondChild: SizedBox(
height: 16, height: 16,
@@ -325,17 +375,23 @@ class MessageInputState extends State<MessageInput> {
return AnimatedCrossFade( return AnimatedCrossFade(
crossFadeState: crossFadeState:
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: InkWell( firstChild: Padding(
onTap: () { padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: IconButton(
onPressed: () {
setState(() { setState(() {
_actionsShrunk = false; _actionsShrunk = false;
}); });
}, },
child: Padding( icon: StreamSvgIcon.emptyCircleLeft(
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), color: StreamChatTheme.of(context).colorTheme.accentBlue,
child: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context).accentColor,
), ),
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
splashRadius: 24,
), ),
), ),
secondChild: Row( secondChild: Row(
@@ -361,13 +417,15 @@ class MessageInputState extends State<MessageInput> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.0), borderRadius: BorderRadius.circular(24.0),
border: Border.all( border: Border.all(
color: Colors.grey, color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
), ),
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0), padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildReplyToMessage(),
_buildAttachments(), _buildAttachments(),
LimitedBox( LimitedBox(
maxHeight: widget.maxHeight, maxHeight: widget.maxHeight,
@@ -408,23 +466,31 @@ class MessageInputState extends State<MessageInput> {
padding: padding:
const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip( child: Chip(
backgroundColor: backgroundColor: StreamChatTheme.of(context)
StreamChatTheme.of(context).accentColor, .colorTheme
.accentBlue,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
labelPadding: labelPadding:
EdgeInsets.symmetric(horizontal: 9.0), EdgeInsets.symmetric(horizontal: 8.0),
label: Row( label: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
StreamSvgIcon.lightning( StreamSvgIcon.lightning(
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
size: 16.0, size: 16.0,
), ),
Text( Text(
_chosenCommand?.name?.toUpperCase() ?? "", _chosenCommand?.name?.toUpperCase() ?? '',
style: TextStyle( style: StreamChatTheme.of(context)
color: Colors.white, fontSize: 12.0), .textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.white),
), ),
], ],
), ),
@@ -432,23 +498,14 @@ class MessageInputState extends State<MessageInput> {
) )
: null, : null,
suffixIcon: _commandEnabled suffixIcon: _commandEnabled
? InkWell( ? IconButton(
child: Padding( icon: StreamSvgIcon.close_small(),
padding: splashRadius: 24,
const EdgeInsets.symmetric(horizontal: 8.0), onPressed: () {
child: StreamSvgIcon.close_small(), setState(() => _commandEnabled = false);
),
onTap: () {
setState(() {
_commandEnabled = false;
});
}, },
) )
: null, : null,
suffixIconConstraints: BoxConstraints(
maxHeight: 24.0,
maxWidth: 40.0,
),
), ),
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
), ),
@@ -602,12 +659,12 @@ class MessageInputState extends State<MessageInput> {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(400)), constraints: BoxConstraints.loose(Size.fromHeight(400)),
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(8.0)), borderRadius: BorderRadius.circular(8.0)),
child: ListView( child: ListView(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -625,13 +682,17 @@ class MessageInputState extends State<MessageInput> {
), ),
child: StreamSvgIcon.lightning( child: StreamSvgIcon.lightning(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.accentColor, .colorTheme
.accentBlue,
), ),
), ),
Text( Text(
'Instant Commands', 'Instant Commands',
style: TextStyle( 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), //subtitle: Text(c.description),
onTap: () { onTap: () {
_setCommand(c); _setCommand(c);
@@ -693,27 +745,36 @@ class MessageInputState extends State<MessageInput> {
switch (index) { switch (index) {
case 0: case 0:
return _attachments.isEmpty return _attachments.isEmpty
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: (!_attachmentContainsFile : (!_attachmentContainsFile
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.black.withOpacity(0.2)); : StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2));
break; break;
case 1: case 1:
return _attachmentContainsFile return _attachmentContainsFile
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: (_attachments.isEmpty : (_attachments.isEmpty
? Colors.black.withOpacity(0.5) ? StreamChatTheme.of(context)
: Colors.black.withOpacity(0.2)); .colorTheme
.black
.withOpacity(0.5)
: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2));
break; break;
case 2: case 2:
return _attachmentContainsFile && _attachments.isNotEmpty return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2) ? StreamChatTheme.of(context).colorTheme.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5); : StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5);
break; break;
case 3: case 3:
return _attachmentContainsFile && _attachments.isNotEmpty return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2) ? StreamChatTheme.of(context).colorTheme.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5); : StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5);
break; break;
} }
} }
@@ -722,7 +783,7 @@ class MessageInputState extends State<MessageInput> {
duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero, duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
height: _openFilePickerSection ? _filePickerSize : 0, height: _openFilePickerSection ? _filePickerSize : 0,
child: Material( child: Material(
color: Color(0xFFF2F2F2), color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column( child: Column(
children: [ children: [
Row( Row(
@@ -789,7 +850,7 @@ class MessageInputState extends State<MessageInput> {
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0), topRight: Radius.circular(16.0),
@@ -804,7 +865,8 @@ class MessageInputState extends State<MessageInput> {
width: 40.0, width: 40.0,
height: 4.0, height: 4.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF2F2F2), color:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
), ),
), ),
@@ -817,7 +879,7 @@ class MessageInputState extends State<MessageInput> {
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
child: _buildPickerSection(), child: _buildPickerSection(),
@@ -852,11 +914,12 @@ class MessageInputState extends State<MessageInput> {
}, },
child: Container( child: Container(
constraints: BoxConstraints.expand(), constraints: BoxConstraints.expand(),
color: Color(0xfff2f2f2), color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Text( child: Text(
'Add more files', 'Add more files',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).accentColor, color:
StreamChatTheme.of(context).colorTheme.accentBlue,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -885,7 +948,7 @@ class MessageInputState extends State<MessageInput> {
PhotoManager.openSetting(); PhotoManager.openSetting();
}, },
child: Container( child: Container(
color: Color(0xFFF2F2F2), color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -894,15 +957,19 @@ class MessageInputState extends State<MessageInput> {
'svgs/icon_picture_empty_state.svg', 'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
height: 140, height: 140,
color: StreamChatTheme.of(context).accentColor, color:
StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
Center( Center(
child: Text( child: Text(
'Allow access to your gallery', 'Allow access to your gallery',
style: TextStyle( style: StreamChatTheme.of(context)
fontSize: 14, .textTheme
fontWeight: FontWeight.bold, .bodyBold
color: StreamChatTheme.of(context).accentColor, .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
), ),
), ),
), ),
@@ -1019,7 +1086,7 @@ class MessageInputState extends State<MessageInput> {
CircleAvatar _buildGiphyIcon() { CircleAvatar _buildGiphyIcon() {
if (kIsWeb) { if (kIsWeb) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Colors.black, backgroundColor: StreamChatTheme.of(context).colorTheme.black,
child: Image.asset( child: Image.asset(
'images/giphy_icon.png', 'images/giphy_icon.png',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
@@ -1080,7 +1147,7 @@ class MessageInputState extends State<MessageInput> {
child: Card( child: Card(
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(8.0),
elevation: 2.0, elevation: 2.0,
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
@@ -1088,7 +1155,7 @@ class MessageInputState extends State<MessageInput> {
child: Container( child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(400)), constraints: BoxConstraints.loose(Size.fromHeight(400)),
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
), ),
child: FutureBuilder<List<Member>>( child: FutureBuilder<List<Member>>(
future: queryMembers ?? Future.value(members), future: queryMembers ?? Future.value(members),
@@ -1116,7 +1183,8 @@ class MessageInputState extends State<MessageInput> {
subtitle: Text('@${m.userId}'), subtitle: Text('@${m.userId}'),
trailing: StreamSvgIcon.mentions( trailing: StreamSvgIcon.mentions(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.accentColor, .colorTheme
.accentBlue,
), ),
onTap: () { onTap: () {
_mentionedUsers.add(m.user); _mentionedUsers.add(m.user);
@@ -1181,7 +1249,7 @@ class MessageInputState extends State<MessageInput> {
child: Card( child: Card(
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(8.0),
elevation: 2.0, elevation: 2.0,
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
@@ -1196,7 +1264,7 @@ class MessageInputState extends State<MessageInput> {
offset: Offset(0, -4), offset: Offset(0, -4),
), ),
], ],
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).colorTheme.white,
), ),
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -1212,14 +1280,19 @@ class MessageInputState extends State<MessageInput> {
padding: padding:
const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 8.0),
child: StreamSvgIcon.smile( child: StreamSvgIcon.smile(
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
), ),
), ),
Flexible( Flexible(
child: Text( child: Text(
'Emoji matching "$query"', 'Emoji matching "$query"',
style: TextStyle( 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; _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() { Widget _buildAttachments() {
return _attachments.isEmpty return _attachments.isEmpty
? Container() ? Container()
@@ -1313,10 +1405,15 @@ class MessageInputState extends State<MessageInput> {
child: InkWell( child: InkWell(
child: CircleAvatar( child: CircleAvatar(
backgroundColor: backgroundColor:
Colors.black.withOpacity(0.2), StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.6),
maxRadius: 12.0, maxRadius: 12.0,
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
color: Colors.white, color: StreamChatTheme.of(context)
.colorTheme
.white,
), ),
), ),
onTap: () { onTap: () {
@@ -1401,11 +1498,11 @@ class MessageInputState extends State<MessageInput> {
_attachments.remove(attachment); _attachments.remove(attachment);
}); });
}, },
fillColor: Colors.black.withOpacity(.5), fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5),
child: Center( child: Center(
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
size: 24, size: 24,
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
), ),
), ),
@@ -1488,17 +1585,21 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildCommandButton() { Widget _buildCommandButton() {
return InkWell( return Padding(
child: Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
padding: child: IconButton(
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0), icon: StreamSvgIcon.lightning(
child: StreamSvgIcon.lightning(
color: _commandsOverlay != null color: _commandsOverlay != null
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Color(0xFF000000).withAlpha(128), : StreamChatTheme.of(context).colorTheme.grey,
), ),
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
), ),
onTap: () async { splashRadius: 24,
onPressed: () async {
if (_openFilePickerSection) { if (_openFilePickerSection) {
setState(() { setState(() {
_animateContainer = false; _animateContainer = false;
@@ -1520,23 +1621,26 @@ class MessageInputState extends State<MessageInput> {
}); });
} }
}, },
),
); );
} }
Widget _buildAttachmentButton() { Widget _buildAttachmentButton() {
var padding = widget.editMessage == null ? 4.0 : 8.0; return Padding(
return Center( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: InkWell( child: IconButton(
child: Padding( icon: StreamSvgIcon.attach(
padding:
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
child: StreamSvgIcon.attach(
color: _openFilePickerSection color: _openFilePickerSection
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Color(0xFF000000).withAlpha(128), : StreamChatTheme.of(context).colorTheme.grey,
), ),
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
), ),
onTap: () async { splashRadius: 24,
onPressed: () async {
_emojiOverlay?.remove(); _emojiOverlay?.remove();
_emojiOverlay = null; _emojiOverlay = null;
_commandsOverlay?.remove(); _commandsOverlay?.remove();
@@ -1854,36 +1958,29 @@ class MessageInputState extends State<MessageInput> {
Widget _buildIdleSendButton(BuildContext context) { Widget _buildIdleSendButton(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: Center(
child: InkWell(
onTap: () {
sendMessage();
},
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: _getIdleSendIcon(), assetName: _getIdleSendIcon(),
color: Colors.grey, color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 24.0,
width: 24.0,
), ),
)),
); );
} }
Widget _buildSendButton(BuildContext context) { Widget _buildSendButton(BuildContext context) {
return Center( return Padding(
child: Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), child: IconButton(
child: InkWell( onPressed: sendMessage,
onTap: () { visualDensity: VisualDensity.compact,
sendMessage(); padding: const EdgeInsets.all(0),
}, splashRadius: 24,
child: StreamSvgIcon( constraints: BoxConstraints.tightFor(
assetName: _getSendIcon(), height: 24,
color: StreamChatTheme.of(context).accentColor, width: 24,
height: 24.0,
width: 24.0,
), ),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: StreamChatTheme.of(context).colorTheme.accentBlue,
), ),
), ),
); );
@@ -1922,6 +2019,9 @@ class MessageInputState extends State<MessageInput> {
textEditingController.clear(); textEditingController.clear();
_attachments.clear(); _attachments.clear();
if (widget.onQuotedMessageCleared != null) {
widget.onQuotedMessageCleared();
}
setState(() { setState(() {
_messageIsPresent = false; _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) { if (widget.preMessageSending != null) {
message = await widget.preMessageSending(message); message = await widget.preMessageSending(message);
} }
@@ -1976,10 +2082,6 @@ class MessageInputState extends State<MessageInput> {
return sendingFuture.then((resp) { return sendingFuture.then((resp) {
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
widget.onMessageSent(resp.message); widget.onMessageSent(resp.message);
} else {
if (widget.editMessage != null) {
Navigator.pop(context);
}
} }
}); });
} }
@@ -1995,7 +2097,6 @@ class MessageInputState extends State<MessageInput> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_focusNode = widget.focusNode ?? FocusNode(); _focusNode = widget.focusNode ?? FocusNode();
_emojiNames = Emoji.all().map((e) => e.name); _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. /// Represents a 2-tuple, or pair.
class Tuple2<T1, T2> { class Tuple2<T1, T2> {
/// Returns the first item of the tuple /// Returns the first item of the tuple
+94 -55
View File
@@ -16,6 +16,7 @@ import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'date_divider.dart'; import 'date_divider.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
import 'swipeable.dart';
typedef MessageBuilder = Widget Function( typedef MessageBuilder = Widget Function(
BuildContext, BuildContext,
@@ -29,6 +30,9 @@ typedef ParentMessageBuilder = Widget Function(
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent); typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
typedef ThreadTapCallback = void Function(Message, Widget); typedef ThreadTapCallback = void Function(Message, Widget);
typedef OnMessageSwiped = void Function(Message);
typedef ReplyTapCallback = void Function(Message);
class MessageDetails { class MessageDetails {
/// True if the message belongs to the current user /// True if the message belongs to the current user
bool isMyMessage; bool isMyMessage;
@@ -109,14 +113,17 @@ class MessageListView extends StatefulWidget {
this.parentMessage, this.parentMessage,
this.threadBuilder, this.threadBuilder,
this.onThreadTap, this.onThreadTap,
this.onReplyTap,
this.dateDividerBuilder, this.dateDividerBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(), this.scrollPhysics = const ClampingScrollPhysics(),
this.initialScrollIndex, this.initialScrollIndex,
this.initialAlignment, this.initialAlignment,
this.scrollController, this.scrollController,
this.itemPositionListener, this.itemPositionListener,
this.onMessageSwiped,
this.highlightInitialMessage = false, this.highlightInitialMessage = false,
this.highlightColor = const Color(0XFFFBF4DD), this.messageHighlightColor,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -158,15 +165,21 @@ class MessageListView extends StatefulWidget {
/// The ScrollPhysics used by the ListView /// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics; 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. /// If true the list will highlight the initialMessage if there is any.
/// ///
/// Also See [StreamChannel] /// Also See [StreamChannel]
final bool highlightInitialMessage; final bool highlightInitialMessage;
/// The color used for highlighting the initialMessage if there is any. /// Color used while highlighting initial message
/// final Color messageHighlightColor;
/// Also See [highlightInitialMessage]
final Color highlightColor; final ShowMessageCallback onShowMessage;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
@@ -179,10 +192,10 @@ class _MessageListViewState extends State<MessageListView> {
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener; ItemPositionsListener _itemPositionListener;
int _messageListLength; int _messageListLength;
StreamChannelState streamChannel;
int get _initialIndex { int get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex; if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
final streamChannel = StreamChannel.of(context);
if (streamChannel.initialMessageId != null) { if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages; final messages = streamChannel.channel.state.messages;
final totalMessages = messages.length; final totalMessages = messages.length;
@@ -202,11 +215,10 @@ class _MessageListViewState extends State<MessageListView> {
} }
bool _isInitialMessage(String id) { bool _isInitialMessage(String id) {
final streamChannel = StreamChannel.of(context);
return streamChannel.initialMessageId == id; return streamChannel.initialMessageId == id;
} }
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate; bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool _topPaginationActive = false; bool _topPaginationActive = false;
bool _bottomPaginationActive = false; bool _bottomPaginationActive = false;
@@ -222,22 +234,13 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
final messagesStream = widget.parentMessage != null final messagesStream = widget.parentMessage != null
? streamChannel.channel.state.threadsStream ? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id)) .where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id]) .map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream; : streamChannel.channel.state?.messagesStream;
return WillPopScope( return StreamBuilder<List<Message>>(
onWillPop: () async {
if (!_upToDate) {
await streamChannel.reloadChannel();
}
return true;
},
child: StreamBuilder<List<Message>>(
stream: messagesStream?.map((messages) => messages stream: messagesStream?.map((messages) => messages
?.where((e) => ?.where((e) =>
!e.isDeleted || !e.isDeleted ||
@@ -252,16 +255,19 @@ class _MessageListViewState extends State<MessageListView> {
} }
final messageList = snapshot.data?.reversed?.toList() ?? []; final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) { if (messageList.isEmpty) {
if (_upToDate) { if (_upToDate) {
return Center( return Center(
child: Text( child: Text(
'No chats here yet...', 'No chats here yet...',
style: TextStyle( style: StreamChatTheme.of(context)
fontSize: 12, .textTheme
color: Colors.black.withOpacity(.5), .footnote
), .copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
), ),
); );
} }
@@ -344,14 +350,9 @@ class _MessageListViewState extends State<MessageListView> {
buildParentMessage(widget.parentMessage), buildParentMessage(widget.parentMessage),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: StreamChatTheme.of(context)
begin: Alignment.topCenter, .colorTheme
end: Alignment.bottomCenter, .bgGradient,
colors: [
Color(0XFFF7F7F7),
Color(0XFFFCFCFC),
],
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -404,7 +405,7 @@ class _MessageListViewState extends State<MessageListView> {
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
messageWidget = Builder( messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder( builder: (context) => widget.messageBuilder(
context, context,
MessageDetails( MessageDetails(
context, context,
@@ -427,8 +428,7 @@ class _MessageListViewState extends State<MessageListView> {
children: <Widget>[ children: <Widget>[
messageWidget, messageWidget,
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(vertical: 12.0),
const EdgeInsets.symmetric(vertical: 12.0),
child: widget.dateDividerBuilder != null child: widget.dateDividerBuilder != null
? widget.dateDividerBuilder( ? widget.dateDividerBuilder(
nextMessage.createdAt.toLocal()) nextMessage.createdAt.toLocal())
@@ -477,8 +477,7 @@ class _MessageListViewState extends State<MessageListView> {
), ),
], ],
); );
}), });
);
} }
Future<void> _paginateData( Future<void> _paginateData(
@@ -498,7 +497,6 @@ class _MessageListViewState extends State<MessageListView> {
} }
Widget _buildScrollToBottom() { Widget _buildScrollToBottom() {
final streamChannel = StreamChannel.of(context);
return StreamBuilder<Tuple2<bool, int>>( return StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2( stream: Rx.combineLatest2(
streamChannel.channel.state.isUpToDateStream, streamChannel.channel.state.isUpToDateStream,
@@ -529,9 +527,9 @@ class _MessageListViewState extends State<MessageListView> {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
FloatingActionButton( FloatingActionButton(
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
child: StreamSvgIcon.down( child: StreamSvgIcon.down(
color: Colors.black, color: StreamChatTheme.of(context).colorTheme.black,
), ),
onPressed: () { onPressed: () {
if (unreadCount > 0) { if (unreadCount > 0) {
@@ -591,7 +589,10 @@ class _MessageListViewState extends State<MessageListView> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container( return Container(
color: Color(0xffd0021B).withAlpha(26), color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Center( child: Center(
child: Text('Error loading messages'), child: Text('Error loading messages'),
), ),
@@ -696,6 +697,7 @@ class _MessageListViewState extends State<MessageListView> {
return MessageWidget( return MessageWidget(
showThreadReplyIndicator: false, showThreadReplyIndicator: false,
showInChannelIndicator: false, showInChannelIndicator: false,
showReplyIndicator: false,
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showUsername: !isMyMessage, showUsername: !isMyMessage,
@@ -720,6 +722,7 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme ? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
onShowMessage: widget.onShowMessage,
); );
} }
@@ -740,7 +743,7 @@ class _MessageListViewState extends State<MessageListView> {
final isNextUser = final isNextUser =
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; 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 final readList = channel.state?.read
?.where((element) => element.user.id != userId) ?.where((element) => element.user.id != userId)
?.where((read) => ?.where((read) =>
@@ -753,6 +756,9 @@ class _MessageListViewState extends State<MessageListView> {
final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final isThreadMessage =
widget.parentMessage != null || message?.showInChannel == true;
Widget child = MessageWidget( Widget child = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
@@ -764,6 +770,26 @@ class _MessageListViewState extends State<MessageListView> {
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
top: 3, 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, showInChannelIndicator: widget.parentMessage == null,
showThreadReplyIndicator: widget.parentMessage == null, showThreadReplyIndicator: widget.parentMessage == null,
showUsername: !isMyMessage && !isNextUser, showUsername: !isMyMessage && !isNextUser,
@@ -776,6 +802,7 @@ class _MessageListViewState extends State<MessageListView> {
showDeleteMessage: isMyMessage, showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null, borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
onReplyTap: widget.onReplyTap,
attachmentBorderRadiusGeometry: BorderRadius.only( attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16), bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
@@ -797,21 +824,33 @@ class _MessageListViewState extends State<MessageListView> {
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
readList: readList, readList: readList,
allRead: allRead, 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 && if (!initialMessageHighlightComplete &&
widget.highlightInitialMessage && widget.highlightInitialMessage &&
_isInitialMessage(message.id)) { _isInitialMessage(message.id)) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
final highlightColor =
widget.messageHighlightColor ?? colorTheme.highlight;
child = TweenAnimationBuilder<Color>( child = TweenAnimationBuilder<Color>(
tween: ColorTween( tween: ColorTween(
begin: widget.highlightColor, begin: highlightColor,
end: Colors.white10, end: colorTheme.white.withOpacity(0),
), ),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
child: child, child: child,
onEnd: () { onEnd: () => initialMessageHighlightComplete = true,
initialMessageHighlightComplete = true;
},
builder: (_, color, child) { builder: (_, color, child) {
return Container( return Container(
color: color, color: color,
@@ -831,7 +870,7 @@ class _MessageListViewState extends State<MessageListView> {
_itemPositionListener = _itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create(); widget.itemPositionListener ?? ItemPositionsListener.create();
final streamChannel = StreamChannel.of(context); streamChannel = StreamChannel.of(context);
initialIndex = _initialIndex; initialIndex = _initialIndex;
initialAlignment = _initialAlignment; initialAlignment = _initialAlignment;
@@ -844,7 +883,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
if (event.message.user.id == streamChannel.channel.client.state.user.id) { if (event.message.user.id == streamChannel.channel.client.state.user.id) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo( _scrollController?.jumpTo(
index: 0, index: 0,
); );
}); });
@@ -874,16 +913,13 @@ class _MessageListViewState extends State<MessageListView> {
context, context,
MaterialPageRoute(builder: (_) { MaterialPageRoute(builder: (_) {
return StreamBuilder<Message>( return StreamBuilder<Message>(
stream: StreamChannel.of(context) stream: streamChannel.channel.state.messagesStream.map(
.channel (messages) =>
.state
.messagesStream
.map((messages) =>
messages.firstWhere((m) => m.id == message.id)), messages.firstWhere((m) => m.id == message.id)),
initialData: message, initialData: message,
builder: (_, snapshot) { builder: (_, snapshot) {
return StreamChannel( return StreamChannel(
channel: StreamChannel.of(context).channel, channel: streamChannel.channel,
child: widget.threadBuilder(context, snapshot.data), child: widget.threadBuilder(context, snapshot.data),
); );
}); });
@@ -895,6 +931,9 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
void dispose() { void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
}
_messageNewListener?.cancel(); _messageNewListener?.cancel();
super.dispose(); super.dispose();
} }
+22 -11
View File
@@ -40,27 +40,35 @@ class MessageReactionsModal extends StatelessWidget {
final user = StreamChat.of(context).user; final user = StreamChat.of(context).user;
final roughMaxSize = 2 * size.width / 3; 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 = final roughSentenceSize =
message.text.length * messageTheme.messageText.fontSize * 1.2; messageTextLength * messageTheme.messageText.fontSize * 1.2;
final divFactor = message.attachments?.isNotEmpty == true final divFactor = message.attachments?.isNotEmpty == true
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () => Navigator.maybePop(context),
Navigator.pop(context);
},
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: 10.8731, sigmaX: 10,
sigmaY: 10.8731, sigmaY: 10,
), ),
child: Container( child: Container(
color: Colors.black.withOpacity(0.1), color: StreamChatTheme.of(context).colorTheme.overlay,
), ),
), ),
), ),
@@ -110,14 +118,16 @@ class MessageReactionsModal extends StatelessWidget {
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
shape: messageShape, shape: messageShape,
showReactionPickerIndicator: true, showInChannelIndicator: false,
showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
), ),
), ),
); );
}), }),
SizedBox( SizedBox(height: 8),
height: 16,
),
if (message.latestReactions?.isNotEmpty == true) if (message.latestReactions?.isNotEmpty == true)
_buildReactionCard(context), _buildReactionCard(context),
], ],
@@ -137,6 +147,7 @@ class MessageReactionsModal extends StatelessWidget {
horizontal: 8.0, horizontal: 8.0,
), ),
child: Card( child: Card(
color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
+2
View File
@@ -55,6 +55,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
/// Calls [Client.search] updating [queryMessagesLoading] stream /// Calls [Client.search] updating [queryMessagesLoading] stream
Future<void> search({ Future<void> search({
Map<String, dynamic> filter, Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort, List<SortOption> sort,
String query, String query,
PaginationParams pagination, PaginationParams pagination,
@@ -78,6 +79,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
sort, sort,
query, query,
pagination, pagination,
messageFilters: messageFilter,
); );
if (clear) { if (clear) {
+53 -7
View File
@@ -113,7 +113,9 @@ class MessageSearchItem extends StatelessWidget {
} else if (e.type == 'giphy') { } else if (e.type == 'giphy') {
return '[GIF]'; return '[GIF]';
} }
return null; return e == message.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}).where((e) => e != null), }).where((e) => e != null),
message.text ?? '', message.text ?? '',
]; ];
@@ -121,17 +123,61 @@ class MessageSearchItem extends StatelessWidget {
text = parts.join(' '); text = parts.join(' ');
} }
return Text( return Text.rich(
_getDisplayText(
text, text,
maxLines: 1, message.mentionedUsers,
overflow: TextOverflow.ellipsis, message.attachments,
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color:
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
fontStyle: (message.isSystem || message.isDeleted) fontStyle: (message.isSystem || message.isDeleted)
? FontStyle.italic ? FontStyle.italic
: FontStyle.normal, : 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,
); );
} }
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);
}
} }
+9 -13
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart';
import '../stream_chat_flutter.dart';
import 'lazy_load_scroll_view.dart'; import 'lazy_load_scroll_view.dart';
import 'message_search_bloc.dart'; import 'message_search_bloc.dart';
@@ -127,8 +128,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
return Container( return Container(
height: 1, height: 1,
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.1) ? StreamChatTheme.of(context).colorTheme.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1), : StreamChatTheme.of(context).colorTheme.black.withOpacity(0.1),
); );
} }
@@ -151,7 +152,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container( return Container(
color: Color(0xffd0021B).withAlpha(26), color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center( child: Center(
@@ -309,15 +313,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
Container( Container(
width: double.maxFinite, width: double.maxFinite,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withOpacity(0.02),
Colors.white.withOpacity(0.05),
],
stops: [0, 1],
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -327,7 +323,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
child: Text( child: Text(
'${items.length} results', '${items.length} results',
style: TextStyle( style: TextStyle(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context).colorTheme.grey,
), ),
), ),
), ),
+1 -1
View File
@@ -31,7 +31,7 @@ class MessageText extends StatelessWidget {
) { ) {
if (link.startsWith('@')) { if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhere( final mentionedUser = message.mentionedUsers.firstWhere(
(u) => '@${u.name.replaceAll(' ', '')}' == link, (u) => '@${u.name}' == link,
orElse: () => null, orElse: () => null,
); );
+190 -209
View File
@@ -10,14 +10,17 @@ import 'package:flutter_portal/flutter_portal.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/message_actions_modal.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/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/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'extension.dart';
import 'image_group.dart'; import 'image_group.dart';
import 'message_text.dart'; import 'message_text.dart';
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
typedef OnQuotedMessageTap = void Function(String);
/// The display behaviour of a widget /// The display behaviour of a widget
enum DisplayWidget { enum DisplayWidget {
@@ -46,6 +49,7 @@ class MessageWidget extends StatefulWidget {
/// The function called when tapping on replies /// The function called when tapping on replies
final void Function(Message) onThreadTap; final void Function(Message) onThreadTap;
final void Function(Message) onReplyTap;
final Widget Function(BuildContext, Message) editMessageInputBuilder; final Widget Function(BuildContext, Message) editMessageInputBuilder;
final Widget Function(BuildContext, Message) textBuilder; final Widget Function(BuildContext, Message) textBuilder;
@@ -102,6 +106,9 @@ class MessageWidget extends StatefulWidget {
/// If true the widget will show the thread reply indicator /// If true the widget will show the thread reply indicator
final bool showThreadReplyIndicator; 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 /// If true the widget will show the show in channel indicator
final bool showInChannelIndicator; final bool showInChannelIndicator;
@@ -114,8 +121,13 @@ class MessageWidget extends StatefulWidget {
/// Used in [MessageReactionsModal] and [MessageActionsModal] /// Used in [MessageReactionsModal] and [MessageActionsModal]
final bool showReactionPickerIndicator; final bool showReactionPickerIndicator;
/// If true the widget will show the resendMessage indicator
final bool showResendMessage;
final List<Read> readList; final List<Read> readList;
final ShowMessageCallback onShowMessage;
/// If true show the users username next to the timestamp of the message /// If true show the users username next to the timestamp of the message
final bool showUsername; final bool showUsername;
final bool showTimestamp; final bool showTimestamp;
@@ -126,6 +138,9 @@ class MessageWidget extends StatefulWidget {
/// Center user avatar with bottom of the message /// Center user avatar with bottom of the message
final bool translateUserAvatar; final bool translateUserAvatar;
/// Function called when quotedMessage is tapped
final OnQuotedMessageTap onQuotedMessageTap;
/// ///
MessageWidget({ MessageWidget({
Key key, Key key,
@@ -145,6 +160,8 @@ class MessageWidget extends StatefulWidget {
this.showSendingIndicator = DisplayWidget.show, this.showSendingIndicator = DisplayWidget.show,
this.showThreadReplyIndicator = true, this.showThreadReplyIndicator = true,
this.showInChannelIndicator = true, this.showInChannelIndicator = true,
this.showReplyIndicator = true,
this.onReplyTap,
this.onThreadTap, this.onThreadTap,
this.showUsername = true, this.showUsername = true,
this.showTimestamp = true, this.showTimestamp = true,
@@ -154,9 +171,11 @@ class MessageWidget extends StatefulWidget {
this.onUserAvatarTap, this.onUserAvatarTap,
this.onLinkTap, this.onLinkTap,
this.onMessageActions, this.onMessageActions,
this.onShowMessage,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.textBuilder, this.textBuilder,
Map<String, AttachmentBuilder> customAttachmentBuilders, Map<String, AttachmentBuilder> customAttachmentBuilders,
this.showResendMessage = true,
this.readList, this.readList,
this.padding, this.padding,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
@@ -165,6 +184,7 @@ class MessageWidget extends StatefulWidget {
), ),
this.attachmentPadding = EdgeInsets.zero, this.attachmentPadding = EdgeInsets.zero,
this.allRead = false, this.allRead = false,
this.onQuotedMessageTap,
}) : attachmentBuilders = { }) : attachmentBuilders = {
'image': (context, message, attachment) { 'image': (context, message, attachment) {
return ImageAttachment( return ImageAttachment(
@@ -175,6 +195,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
onShowMessage: onShowMessage,
); );
}, },
'video': (context, message, attachment) { 'video': (context, message, attachment) {
@@ -186,6 +207,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
message: message, message: message,
onShowMessage: onShowMessage,
); );
}, },
'giphy': (context, message, attachment) { 'giphy': (context, message, attachment) {
@@ -197,6 +219,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
onShowMessage: onShowMessage,
); );
}, },
'file': (context, message, attachment) { 'file': (context, message, attachment) {
@@ -224,22 +247,53 @@ class _MessageWidgetState extends State<MessageWidget> {
bool get showTimeStamp => bool get showTimeStamp =>
widget.message.createdAt != null && widget.showTimestamp; widget.message.createdAt != null && widget.showTimestamp;
bool get showReadList => widget.readList?.isNotEmpty == true; bool get isMessageRead => widget.readList?.isNotEmpty == true;
bool get showInChannel => bool get showInChannel =>
widget.showInChannelIndicator && widget.message?.showInChannel == true; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var leftPadding = widget.showUserAvatar != DisplayWidget.gone final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5 var leftPadding =
: 6.0; widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 4.5;
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
final hasFiles = final hasFiles =
widget.message.attachments?.any((element) => element.type == 'file') == widget.message.attachments?.any((element) => element.type == 'file') ==
true; true;
return Portal( return Material(
type: MaterialType.transparency,
child: Portal(
child: InkWell(
onLongPress: widget.message.isDeleted && !isFailedState
? null
: () => onLongPress(context),
child: Padding( child: Padding(
padding: widget.padding ?? EdgeInsets.all(8), padding: widget.padding ?? EdgeInsets.all(8),
child: Transform( child: Transform(
@@ -253,6 +307,7 @@ class _MessageWidgetState extends State<MessageWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Stack( Stack(
clipBehavior: Clip.none,
alignment: AlignmentDirectional.bottomStart, alignment: AlignmentDirectional.bottomStart,
children: [ children: [
Column( Column(
@@ -264,15 +319,13 @@ class _MessageWidgetState extends State<MessageWidget> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show) if (widget.showUserAvatar ==
DisplayWidget.show) ...[
_buildUserAvatar(), _buildUserAvatar(),
SizedBox(width: 6), SizedBox(width: 4),
],
if (widget.showUserAvatar == DisplayWidget.hide) if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox( SizedBox(width: avatarWidth + 4),
width: widget.messageTheme.avatarTheme
.constraints.maxWidth +
8,
),
Flexible( Flexible(
child: PortalEntry( child: PortalEntry(
portal: Container( portal: Container(
@@ -290,7 +343,9 @@ class _MessageWidgetState extends State<MessageWidget> {
Padding( Padding(
padding: widget.showReactions padding: widget.showReactions
? EdgeInsets.only( ? EdgeInsets.only(
top: widget.message.reactionCounts top: widget
.message
.reactionCounts
?.isNotEmpty == ?.isNotEmpty ==
true true
? 18 ? 18
@@ -298,41 +353,36 @@ class _MessageWidgetState extends State<MessageWidget> {
) )
: EdgeInsets.zero, : EdgeInsets.zero,
child: (widget.message.isDeleted && child: (widget.message.isDeleted &&
widget.message.status != !isFailedState)
MessageSendingStatus
.FAILED_DELETE)
? Transform( ? Transform(
alignment: Alignment.center, alignment: Alignment.center,
transform: Matrix4.rotationY( transform: Matrix4.rotationY(
widget.reverse ? pi : 0), widget.reverse ? pi : 0),
child: DeletedMessage( child: DeletedMessage(
reverse: widget.reverse, reverse: widget.reverse,
borderRadiusGeometry: borderRadiusGeometry: widget
widget.borderRadiusGeometry, .borderRadiusGeometry,
borderSide: widget.borderSide, borderSide:
widget.borderSide,
shape: widget.shape, shape: widget.shape,
messageTheme: messageTheme:
widget.messageTheme, widget.messageTheme,
), ),
) )
: Material( : Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
elevation: 0.0,
shape: widget.shape ?? shape: widget.shape ??
RoundedRectangleBorder( RoundedRectangleBorder(
side: isOnlyEmoji side: isOnlyEmoji &&
!(showThreadReplyIndicator ||
showInChannel)
? BorderSide.none ? BorderSide.none
: widget.borderSide ?? : widget.borderSide ??
BorderSide( BorderSide(
color: Theme.of(context) color: widget
.brightness == .messageTheme
Brightness .messageBorderColor,
.dark
? Colors.white
.withAlpha(
24)
: Colors.black
.withAlpha(
24),
), ),
borderRadius: widget borderRadius: widget
.borderRadiusGeometry ?? .borderRadiusGeometry ??
@@ -344,17 +394,21 @@ class _MessageWidgetState extends State<MessageWidget> {
hasFiles ? 2.0 : 0.0), hasFiles ? 2.0 : 0.0),
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment
.start,
mainAxisSize: mainAxisSize:
MainAxisSize.min, MainAxisSize.min,
children: <Widget>[ children: <Widget>[
if (hasQuotedMessage)
_buildQuotedMessage(),
..._parseAttachments( ..._parseAttachments(
context), context),
if (widget.message.text if (widget.message.text
.trim() .trim()
.isNotEmpty && .isNotEmpty &&
!isGiphy) !isGiphy)
_buildTextBubble(context), _buildTextBubble(
context),
], ],
), ),
), ),
@@ -383,18 +437,23 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
], ],
), ),
if (showThreadReplyIndicator || if (showBottomRow) SizedBox(height: 20.0),
showUsername ||
showTimeStamp ||
showInChannel)
SizedBox(height: 20.0),
], ],
), ),
if (showThreadReplyIndicator || if (showBottomRow) _buildBottomRow(leftPadding),
showUsername || if (isFailedState)
showTimeStamp || Positioned(
showInChannel) left: widget.reverse ? -3 : null,
_buildBottomRow(leftPadding) right: widget.reverse ? null : -9,
bottom: showBottomRow ? 20 : 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: StreamSvgIcon.error(size: 20),
),
),
], ],
), ),
], ],
@@ -402,6 +461,25 @@ 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,
); );
} }
@@ -414,16 +492,18 @@ class _MessageWidgetState extends State<MessageWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
StreamSvgIcon.eye( StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
size: 16.0, size: 16.0,
), ),
SizedBox(width: 8.0), SizedBox(width: 8.0),
Text( Text(
'Only visible to you', 'Only visible to you',
style: TextStyle( style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
fontSize: 12.0, .colorTheme
), .black
.withOpacity(0.5)),
), ),
], ],
), ),
@@ -449,6 +529,8 @@ class _MessageWidgetState extends State<MessageWidget> {
} else { } else {
final showSendingIndicator = final showSendingIndicator =
widget.showSendingIndicator == DisplayWidget.show; widget.showSendingIndicator == DisplayWidget.show;
final threadParticipants = widget.message.threadParticipants;
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
final replyCount = widget.message.replyCount; final replyCount = widget.message.replyCount;
final msg = replyCount != 0 final msg = replyCount != 0
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
@@ -461,19 +543,17 @@ class _MessageWidgetState extends State<MessageWidget> {
children.addAll([ children.addAll([
if (showSendingIndicator) _buildSendingIndicator(), if (showSendingIndicator) _buildSendingIndicator(),
if (showReadList) if (showThreadReplyIndicator) ...[
if (showThreadParticipants)
SizedBox.fromSize( SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17), size: Size((threadParticipants.length * 8.0) + 10, 16),
child: Padding( child: _buildThreadParticipantsIndicator(),
padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(),
), ),
),
if (showThreadReplyIndicator)
InkWell( InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null, onTap: widget.onThreadTap != null ? onThreadTap : null,
child: Text(msg, style: widget.messageTheme?.replies), child: Text(msg, style: widget.messageTheme?.replies),
), ),
],
if (showUsername) if (showUsername)
Text( Text(
widget.message.user.name, widget.message.user.name,
@@ -502,7 +582,8 @@ class _MessageWidgetState extends State<MessageWidget> {
child: CustomPaint( child: CustomPaint(
size: const Size(16, 32), size: const Size(16, 32),
painter: _ThreadReplyPainter( painter: _ThreadReplyPainter(
color: widget.messageTheme.replyThreadColor, context: context,
color: widget.messageTheme.messageBorderColor,
), ),
), ),
), ),
@@ -510,7 +591,7 @@ class _MessageWidgetState extends State<MessageWidget> {
(child) => Transform( (child) => Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: child, child: Container(height: 16, child: Center(child: child)),
), ),
), ),
].insertBetween(const SizedBox(width: 8.0)), ].insertBetween(const SizedBox(width: 8.0)),
@@ -526,7 +607,7 @@ class _MessageWidgetState extends State<MessageWidget> {
var splitList = host.split('.'); var splitList = host.split('.');
var hostName = splitList.length == 3 ? splitList[1] : splitList[0]; var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
var hostDisplayName = urlAttachment.authorName?.capitalize() ?? var hostDisplayName = urlAttachment.authorName?.capitalize() ??
_getWebsiteName(hostName.toLowerCase()) ?? getWebsiteName(hostName.toLowerCase()) ??
hostName.capitalize(); hostName.capitalize();
return UrlAttachment( return UrlAttachment(
@@ -536,14 +617,10 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
bool get isGiphy => Widget _buildThreadParticipantsIndicator() {
widget.message.attachments?.any((element) => element.type == 'giphy') ==
true;
Widget _buildReadIndicator() {
var padding = 0.0; var padding = 0.0;
return Stack( return Stack(
children: widget.readList.map((e) { children: widget.message.threadParticipants.map((user) {
padding += 10.0; padding += 10.0;
return Positioned( return Positioned(
left: padding - 10, left: padding - 10,
@@ -553,15 +630,12 @@ class _MessageWidgetState extends State<MessageWidget> {
color: Colors.white, color: Colors.white,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: CircleBorder(), shape: CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: UserAvatar( child: UserAvatar(
user: e.user, user: user,
constraints: BoxConstraints.loose(Size.fromRadius(8)), constraints: BoxConstraints.loose(Size.fromRadius(8)),
showOnlineStatus: false, showOnlineStatus: false,
), ),
), ),
),
); );
}).toList(), }).toList(),
); );
@@ -604,6 +678,7 @@ class _MessageWidgetState extends State<MessageWidget> {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) {
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
@@ -618,14 +693,22 @@ class _MessageWidgetState extends State<MessageWidget> {
showDeleteMessage: widget.showDeleteMessage, showDeleteMessage: widget.showDeleteMessage,
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onThreadTap: widget.onThreadTap, onReplyTap: widget.onReplyTap,
showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true, onThreadReplyTap: widget.onThreadTap,
showResendMessage:
widget.showResendMessage && (isSendFailed || isUpdateFailed),
showCopyMessage: !isFailedState &&
widget.message.text?.trim()?.isNotEmpty == true,
showEditMessage: widget.showEditMessage && showEditMessage: widget.showEditMessage &&
!isDeleteFailed &&
widget.message.attachments widget.message.attachments
?.any((element) => element.type == 'giphy') != ?.any((element) => element.type == 'giphy') !=
true, true,
showReactions: widget.showReactions, showReactions: widget.showReactions,
showReply: showReply: widget.showReplyIndicator &&
!isFailedState &&
widget.onReplyTap != null,
showThreadReply:
widget.showThreadReplyIndicator && widget.onThreadTap != null, widget.showThreadReplyIndicator && widget.onThreadTap != null,
), ),
); );
@@ -636,6 +719,7 @@ class _MessageWidgetState extends State<MessageWidget> {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) {
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
@@ -663,8 +747,8 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.borderSide ?? widget.borderSide ??
BorderSide( BorderSide(
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24) ? StreamChatTheme.of(context).colorTheme.white.withAlpha(24)
: Colors.black.withAlpha(24), : StreamChatTheme.of(context).colorTheme.black.withAlpha(24),
), ),
borderRadius: widget.attachmentBorderRadiusGeometry ?? borderRadius: widget.attachmentBorderRadiusGeometry ??
widget.borderRadiusGeometry ?? widget.borderRadiusGeometry ??
@@ -692,6 +776,7 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
images: images, images: images,
message: widget.message, message: widget.message,
onShowMessage: widget.onShowMessage,
), ),
), ),
), ),
@@ -728,10 +813,7 @@ class _MessageWidgetState extends State<MessageWidget> {
}) { }) {
final attachmentShape = final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return GestureDetector( return Material(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
child: Material(
color: _getBackgroundColor(), color: _getBackgroundColor(),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: attachmentShape, shape: attachmentShape,
@@ -744,18 +826,7 @@ class _MessageWidgetState extends State<MessageWidget> {
child: Transform( child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Column( child: attachmentWidget,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
getFailedMessageWidget(
context,
padding: const EdgeInsets.all(8.0),
),
attachmentWidget,
],
),
),
), ),
), ),
), ),
@@ -777,21 +848,33 @@ class _MessageWidgetState extends State<MessageWidget> {
} }
Widget _buildSendingIndicator() { Widget _buildSendingIndicator() {
return Container( final style = widget.messageTheme.createdAt;
height: widget.messageTheme.createdAt.fontSize + 2, Widget child = SendingIndicator(
width: widget.messageTheme.createdAt.fontSize + 2,
child: SendingIndicator(
message: widget.message, message: widget.message,
allRead: widget.allRead, 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( Widget _buildUserAvatar() => Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Transform.translate( child: Transform.translate(
offset: Offset( offset: Offset(
0, 0,
@@ -806,60 +889,10 @@ class _MessageWidgetState extends State<MessageWidget> {
showOnlineStatus: false, 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 _buildTextBubble(BuildContext context) {
Widget child = Transform( return Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Column( child: Column(
@@ -867,11 +900,7 @@ class _MessageWidgetState extends State<MessageWidget> {
children: [ children: [
Padding( Padding(
padding: widget.textPadding, padding: widget.textPadding,
child: Column( child: widget.textBuilder != null
crossAxisAlignment: CrossAxisAlignment.end,
children: [
getFailedMessageWidget(context),
widget.textBuilder != null
? widget.textBuilder(context, widget.message) ? widget.textBuilder(context, widget.message)
: MessageText( : MessageText(
onLinkTap: widget.onLinkTap, onLinkTap: widget.onLinkTap,
@@ -885,21 +914,15 @@ class _MessageWidgetState extends State<MessageWidget> {
)) ))
: widget.messageTheme, : widget.messageTheme,
), ),
],
),
), ),
if (widget.message.attachments if (widget.message.attachments
?.any((element) => element.ogScrapeUrl != null) == ?.any((element) => element.ogScrapeUrl != null) ==
true) true &&
!hasQuotedMessage)
_buildUrlAttachment(), _buildUrlAttachment(),
], ],
), ),
); );
return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
child: child,
);
} }
bool get isOnlyEmoji => bool get isOnlyEmoji =>
@@ -907,16 +930,14 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.message.text.characters.every((c) => Emoji.byChar(c) != null); widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Color _getBackgroundColor() { Color _getBackgroundColor() {
if ((widget.message.status == MessageSendingStatus.FAILED || if (hasQuotedMessage) {
widget.message.status == MessageSendingStatus.FAILED_UPDATE || return widget.messageTheme.messageBackgroundColor;
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
return Color(0xffd0021B).withOpacity(.1);
} }
if (widget.message.attachments if (widget.message.attachments
?.any((element) => element.ogScrapeUrl != null) == ?.any((element) => element.ogScrapeUrl != null) ==
true) { true) {
return Color(0xFFE9F2FF); return StreamChatTheme.of(context).colorTheme.blueAlice;
} }
if (isOnlyEmoji) { if (isOnlyEmoji) {
@@ -952,58 +973,18 @@ class _MessageWidgetState extends State<MessageWidget> {
return; 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 { class _ThreadReplyPainter extends CustomPainter {
final Color color; final Color color;
final BuildContext context;
const _ThreadReplyPainter({@required this.color}); const _ThreadReplyPainter({this.context, @required this.color});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = Paint() final paint = Paint()
..color = color ?? Color(0XFFDBDBDB) ..color = color ?? StreamChatTheme.of(context).colorTheme.greyGainsboro
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 1 ..strokeWidth = 1
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
+81
View File
@@ -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(),
),
),
),
],
),
),
),
),
],
);
}
}
+321
View File
@@ -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;
}
}
+10 -4
View File
@@ -107,16 +107,22 @@ class ReactionBubble extends StatelessWidget {
height: 16, height: 16,
color: (!highlightOwnReactions || color: (!highlightOwnReactions ||
reaction.user.id == StreamChat.of(context).user.id) reaction.user.id == StreamChat.of(context).user.id)
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.black.withOpacity(.5), : StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
) )
: Icon( : Icon(
Icons.help_outline_rounded, Icons.help_outline_rounded,
size: 16, size: 16,
color: (!highlightOwnReactions || color: (!highlightOwnReactions ||
reaction.user.id == StreamChat.of(context).user.id) reaction.user.id == StreamChat.of(context).user.id)
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.black.withOpacity(.5), : StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
); );
} }
+8 -6
View File
@@ -89,7 +89,9 @@ class _ReactionPickerState extends State<ReactionPicker>
height: animations[index].value * 24.0, height: animations[index].value * 24.0,
width: animations[index].value * 24.0, width: animations[index].value * 24.0,
color: ownReactionIndex != -1 color: ownReactionIndex != -1
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context)
.colorTheme
.accentBlue
: Theme.of(context) : Theme.of(context)
.iconTheme .iconTheme
.color .color
@@ -134,11 +136,11 @@ class _ReactionPickerState extends State<ReactionPicker>
/// Add a reaction to the message /// Add a reaction to the message
void sendReaction(BuildContext context, String reactionType) { void sendReaction(BuildContext context, String reactionType) {
StreamChannel.of(context) StreamChannel.of(context).channel.sendReaction(
.channel widget.message,
.sendReaction(widget.message, reactionType, extraData: { reactionType,
'enforce_unique': true, enforceUnique: true,
}); );
pop(); pop();
} }
+11 -20
View File
@@ -4,27 +4,27 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Used to show the sending status of the message /// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget { class SendingIndicator extends StatelessWidget {
final Message message; final Message message;
final bool allRead; final bool isMessageRead;
final double size;
const SendingIndicator({ const SendingIndicator({
Key key, Key key,
this.message, this.message,
this.allRead = false, this.isMessageRead = false,
this.size = 12,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (allRead) { if (isMessageRead) {
return Icon( return StreamSvgIcon.checkAll(
Icons.done_all, size: size,
size: 8, color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context).accentColor,
); );
} }
if (message.status == MessageSendingStatus.SENT || message.status == null) { if (message.status == MessageSendingStatus.SENT || message.status == null) {
return Icon( return StreamSvgIcon.check(
Icons.done, size: size,
size: 8,
color: IconTheme.of(context).color.withOpacity(0.5), color: IconTheme.of(context).color.withOpacity(0.5),
); );
} }
@@ -32,18 +32,9 @@ class SendingIndicator extends StatelessWidget {
message.status == MessageSendingStatus.UPDATING) { message.status == MessageSendingStatus.UPDATING) {
return Icon( return Icon(
Icons.access_time, 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(); return SizedBox();
} }
} }
+4 -77
View File
@@ -76,8 +76,9 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
return Theme( return Theme(
data: materialTheme.copyWith( data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme, primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.accentColor, accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.backgroundColor, scaffoldBackgroundColor: streamTheme.colorTheme.white,
buttonTheme: streamTheme.buttonTheme,
), ),
child: widget.child, child: widget.child,
); );
@@ -91,81 +92,7 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
StreamChatThemeData themeData, StreamChatThemeData themeData,
) { ) {
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context)); final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
final theme = defaultTheme.copyWith( return defaultTheme.merge(themeData) ?? themeData;
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;
} }
/// The current user /// The current user
+526 -183
View File
@@ -42,17 +42,14 @@ class StreamChatTheme extends InheritedWidget {
/// Theme data /// Theme data
class StreamChatThemeData { class StreamChatThemeData {
/// Primary color of the chat widgets /// The text themes used in the widgets
final Color primaryColor; final TextTheme textTheme;
/// Secondary color of the chat widgets /// The button themes used in the widgets
final Color secondaryColor; final ButtonThemeData buttonTheme;
/// Accent color of the chat widgets /// The text themes used in the widgets
final Color accentColor; final ColorTheme colorTheme;
/// Background color of the chat widgets
final Color backgroundColor;
/// Theme of the [ChannelPreview] /// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme; final ChannelPreviewTheme channelPreviewTheme;
@@ -79,11 +76,10 @@ class StreamChatThemeData {
final List<ReactionIcon> reactionIcons; final List<ReactionIcon> reactionIcons;
/// Create a theme from scratch /// Create a theme from scratch
StreamChatThemeData({ const StreamChatThemeData({
this.primaryColor, this.textTheme,
this.secondaryColor, this.buttonTheme,
this.accentColor, this.colorTheme,
this.backgroundColor,
this.channelPreviewTheme, this.channelPreviewTheme,
this.channelTheme, this.channelTheme,
this.otherMessageTheme, this.otherMessageTheme,
@@ -97,44 +93,25 @@ class StreamChatThemeData {
/// Create a theme from a Material [Theme] /// Create a theme from a Material [Theme]
factory StreamChatThemeData.fromTheme(ThemeData theme) { factory StreamChatThemeData.fromTheme(ThemeData theme) {
final defaultTheme = getDefaultTheme(theme); final defaultTheme = getDefaultTheme(theme);
final customizedTheme = StreamChatThemeData(
return defaultTheme.copyWith(
accentColor: theme.accentColor,
primaryIconTheme: theme.primaryIconTheme, primaryIconTheme: theme.primaryIconTheme,
primaryColor: theme.colorScheme.primary, ownMessageTheme: MessageTheme(
secondaryColor: theme.colorScheme.secondary, replies: TextStyle(color: theme.accentColor),
backgroundColor: theme.scaffoldBackgroundColor, messageLinks: TextStyle(color: theme.accentColor),
channelTheme: defaultTheme.channelTheme.copyWith(
inputGradient: LinearGradient(colors: [
theme.accentColor.withOpacity(.5),
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. /// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
StreamChatThemeData copyWith({ StreamChatThemeData copyWith({
Color primaryColor, TextTheme textTheme,
Color secondaryColor, ButtonThemeData buttonTheme,
Color accentColor, ColorTheme colorTheme,
Color backgroundColor,
ChannelPreviewTheme channelPreviewTheme, ChannelPreviewTheme channelPreviewTheme,
ChannelTheme channelTheme, ChannelTheme channelTheme,
MessageTheme ownMessageTheme, MessageTheme ownMessageTheme,
@@ -145,91 +122,63 @@ class StreamChatThemeData {
List<ReactionIcon> reactionIcons, List<ReactionIcon> reactionIcons,
}) => }) =>
StreamChatThemeData( StreamChatThemeData(
primaryColor: primaryColor ?? this.primaryColor, textTheme: textTheme ?? this.textTheme,
secondaryColor: secondaryColor ?? this.secondaryColor, buttonTheme: buttonTheme ?? this.buttonTheme,
colorTheme: colorTheme ?? this.colorTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme, primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentColor: accentColor ?? this.accentColor,
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage, defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage, defaultUserImage: defaultUserImage ?? this.defaultUserImage,
backgroundColor: backgroundColor ?? this.backgroundColor, channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
channelPreviewTheme: channelPreviewTheme?.copyWith( channelTheme: channelTheme ?? this.channelTheme,
title: ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
channelPreviewTheme.title ?? this.channelPreviewTheme.title, otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
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,
reactionIcons: reactionIcons ?? this.reactionIcons, 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 /// Get the default Stream Chat theme
static StreamChatThemeData getDefaultTheme(ThemeData theme) { static StreamChatThemeData getDefaultTheme(ThemeData theme) {
final accentColor = Color(0xff006cff); final accentColor = Color(0xff006cff);
final isDark = theme.brightness == Brightness.dark; final isDark = theme.brightness == Brightness.dark;
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
return StreamChatThemeData( return StreamChatThemeData(
secondaryColor: Color(0xffEAEAEA), textTheme: textTheme,
accentColor: accentColor, colorTheme: colorTheme,
primaryColor: isDark ? Colors.black : Colors.white, buttonTheme: ButtonThemeData(
primaryIconTheme: IconThemeData( height: 48.0,
color: isDark ? Colors.white : Colors.black.withOpacity(.5)), 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(), defaultChannelImage: (context, channel) => SizedBox(),
backgroundColor: isDark ? Colors.black : Colors.white,
defaultUserImage: (context, user) => Center( defaultUserImage: (context, user) => Center(
child: CachedNetworkImage( child: CachedNetworkImage(
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
@@ -238,7 +187,7 @@ class StreamChatThemeData {
), ),
), ),
channelPreviewTheme: ChannelPreviewTheme( channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: Color(0xffff3742), unreadCounterColor: colorTheme.accentRed,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -246,21 +195,14 @@ class StreamChatThemeData {
width: 40, width: 40,
), ),
), ),
title: TextStyle( title: textTheme.bodyBold,
fontSize: 14, subtitle: textTheme.footnote.copyWith(
color: isDark ? Colors.white : Colors.black, color: Color(0xff7A7A7A),
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),
), ),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5),
), ),
indicatorIconSize: 16.0),
channelTheme: ChannelTheme( channelTheme: ChannelTheme(
messageInputButtonIconTheme: theme.iconTheme.copyWith( messageInputButtonIconTheme: theme.iconTheme.copyWith(
color: accentColor, color: accentColor,
@@ -273,34 +215,25 @@ class StreamChatThemeData {
width: 40, width: 40,
), ),
), ),
color: isDark ? Colors.black : Colors.white, color: colorTheme.white,
title: TextStyle( title: TextStyle(
fontSize: 14, fontSize: 14,
color: isDark ? Colors.white : Colors.black, color: colorTheme.black,
), ),
lastMessageAt: TextStyle( lastMessageAt: TextStyle(
fontSize: 11, fontSize: 11,
color: isDark color: colorTheme.black.withOpacity(.5),
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
), ),
), ),
inputBackground: inputBackground: colorTheme.white.withAlpha(12),
isDark ? Colors.black.withAlpha(12) : Colors.white.withAlpha(12),
inputGradient: LinearGradient(colors: [
Color(0xFF00AEFF),
Color(0xFF0076FF),
]),
), ),
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageText: TextStyle( messageText: TextStyle(
fontSize: 14.5, fontSize: 14.5,
color: isDark ? Colors.white : Colors.black, color: colorTheme.black,
), ),
createdAt: TextStyle( createdAt: TextStyle(
color: isDark color: colorTheme.black.withOpacity(.5),
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 12, fontSize: 12,
), ),
replies: TextStyle( replies: TextStyle(
@@ -308,10 +241,10 @@ class StreamChatThemeData {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12, fontSize: 12,
), ),
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: isDark ? Colors.black : Colors.white, reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), reactionsBorderColor: colorTheme.greyWhisper,
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -324,17 +257,14 @@ class StreamChatThemeData {
), ),
), ),
otherMessageTheme: MessageTheme( otherMessageTheme: MessageTheme(
reactionsBackgroundColor: reactionsBackgroundColor: colorTheme.greyGainsboro,
isDark ? Color(0xff191919) : Color(0xffEAEAEA), reactionsBorderColor: colorTheme.white,
reactionsBorderColor: isDark ? Colors.black : Colors.white,
messageText: TextStyle( messageText: TextStyle(
fontSize: 14.5, fontSize: 14.5,
color: isDark ? Colors.white : Colors.black, color: colorTheme.black,
), ),
createdAt: TextStyle( createdAt: TextStyle(
color: isDark color: colorTheme.black.withOpacity(.5),
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 12, fontSize: 12,
), ),
replies: TextStyle( replies: TextStyle(
@@ -345,9 +275,8 @@ class StreamChatThemeData {
messageLinks: TextStyle( messageLinks: TextStyle(
color: accentColor, color: accentColor,
), ),
messageBackgroundColor: isDark ? Colors.black : Colors.white, messageBackgroundColor: colorTheme.white,
replyThreadColor: messageBorderColor: colorTheme.greyWhisper,
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -362,19 +291,19 @@ class StreamChatThemeData {
assetName: 'Icon_love_reaction.svg', assetName: 'Icon_love_reaction.svg',
), ),
ReactionIcon( ReactionIcon(
type: 'thumbs_up', type: 'like',
assetName: 'Icon_thumbs_up_reaction.svg', assetName: 'Icon_thumbs_up_reaction.svg',
), ),
ReactionIcon( ReactionIcon(
type: 'thumbs_down', type: 'sad',
assetName: 'Icon_thumbs_down_reaction.svg', assetName: 'Icon_thumbs_down_reaction.svg',
), ),
ReactionIcon( ReactionIcon(
type: 'lol', type: 'haha',
assetName: 'Icon_LOL_reaction.svg', assetName: 'Icon_LOL_reaction.svg',
), ),
ReactionIcon( ReactionIcon(
type: 'wut', type: 'wow',
assetName: 'Icon_wut_reaction.svg', 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 /// Channel theme data
class ChannelTheme { class ChannelTheme {
/// Theme of the [ChannelHeader] widget /// Theme of the [ChannelHeader] widget
@@ -393,9 +653,6 @@ class ChannelTheme {
/// Theme of the send button in [MessageInput] /// Theme of the send button in [MessageInput]
final ButtonThemeData messageInputButtonTheme; final ButtonThemeData messageInputButtonTheme;
/// Gradient of [MessageInput]
final Gradient inputGradient;
/// Background color of [MessageInput] /// Background color of [MessageInput]
final Color inputBackground; final Color inputBackground;
@@ -404,7 +661,6 @@ class ChannelTheme {
this.messageInputButtonIconTheme, this.messageInputButtonIconTheme,
this.messageInputButtonTheme, this.messageInputButtonTheme,
this.inputBackground, this.inputBackground,
this.inputGradient,
}); });
/// Creates a copy of [ChannelTheme] with specified attributes overridden. /// Creates a copy of [ChannelTheme] with specified attributes overridden.
@@ -412,26 +668,29 @@ class ChannelTheme {
ChannelHeaderTheme channelHeaderTheme, ChannelHeaderTheme channelHeaderTheme,
IconThemeData messageInputButtonIconTheme, IconThemeData messageInputButtonIconTheme,
ButtonThemeData messageInputButtonTheme, ButtonThemeData messageInputButtonTheme,
Gradient inputGradient,
Color inputBackground, Color inputBackground,
}) => }) =>
ChannelTheme( ChannelTheme(
channelHeaderTheme: channelHeaderTheme?.copyWith( channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
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,
messageInputButtonIconTheme: messageInputButtonIconTheme:
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme, messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
messageInputButtonTheme: messageInputButtonTheme:
messageInputButtonTheme ?? this.messageInputButtonTheme, messageInputButtonTheme ?? this.messageInputButtonTheme,
inputGradient: inputGradient ?? this.inputGradient,
inputBackground: inputBackground ?? this.inputBackground, 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 { class AvatarTheme {
@@ -451,6 +710,14 @@ class AvatarTheme {
constraints: constraints ?? this.constraints, constraints: constraints ?? this.constraints,
borderRadius: borderRadius ?? this.borderRadius, borderRadius: borderRadius ?? this.borderRadius,
); );
AvatarTheme merge(AvatarTheme other) {
if (other == null) return this;
return copyWith(
constraints: other.constraints,
borderRadius: other.borderRadius,
);
}
} }
class MessageTheme { class MessageTheme {
@@ -460,9 +727,9 @@ class MessageTheme {
final TextStyle createdAt; final TextStyle createdAt;
final TextStyle replies; final TextStyle replies;
final Color messageBackgroundColor; final Color messageBackgroundColor;
final Color messageBorderColor;
final Color reactionsBackgroundColor; final Color reactionsBackgroundColor;
final Color reactionsBorderColor; final Color reactionsBorderColor;
final Color replyThreadColor;
final AvatarTheme avatarTheme; final AvatarTheme avatarTheme;
const MessageTheme({ const MessageTheme({
@@ -471,9 +738,9 @@ class MessageTheme {
this.messageAuthor, this.messageAuthor,
this.messageLinks, this.messageLinks,
this.messageBackgroundColor, this.messageBackgroundColor,
this.messageBorderColor,
this.reactionsBackgroundColor, this.reactionsBackgroundColor,
this.reactionsBorderColor, this.reactionsBorderColor,
this.replyThreadColor,
this.avatarTheme, this.avatarTheme,
this.createdAt, this.createdAt,
}); });
@@ -485,10 +752,10 @@ class MessageTheme {
TextStyle createdAt, TextStyle createdAt,
TextStyle replies, TextStyle replies,
Color messageBackgroundColor, Color messageBackgroundColor,
Color messageBorderColor,
AvatarTheme avatarTheme, AvatarTheme avatarTheme,
Color reactionsBackgroundColor, Color reactionsBackgroundColor,
Color reactionsBorderColor, Color reactionsBorderColor,
Color replyThreadColor,
}) => }) =>
MessageTheme( MessageTheme(
messageText: messageText ?? this.messageText, messageText: messageText ?? this.messageText,
@@ -497,13 +764,31 @@ class MessageTheme {
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
messageBackgroundColor: messageBackgroundColor:
messageBackgroundColor ?? this.messageBackgroundColor, messageBackgroundColor ?? this.messageBackgroundColor,
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
avatarTheme: avatarTheme ?? this.avatarTheme, avatarTheme: avatarTheme ?? this.avatarTheme,
replies: replies ?? this.replies, replies: replies ?? this.replies,
reactionsBackgroundColor: reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, 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 { class ChannelPreviewTheme {
@@ -512,6 +797,7 @@ class ChannelPreviewTheme {
final TextStyle lastMessageAt; final TextStyle lastMessageAt;
final AvatarTheme avatarTheme; final AvatarTheme avatarTheme;
final Color unreadCounterColor; final Color unreadCounterColor;
final double indicatorIconSize;
const ChannelPreviewTheme({ const ChannelPreviewTheme({
this.title, this.title,
@@ -519,6 +805,7 @@ class ChannelPreviewTheme {
this.lastMessageAt, this.lastMessageAt,
this.avatarTheme, this.avatarTheme,
this.unreadCounterColor, this.unreadCounterColor,
this.indicatorIconSize,
}); });
ChannelPreviewTheme copyWith({ ChannelPreviewTheme copyWith({
@@ -527,6 +814,7 @@ class ChannelPreviewTheme {
TextStyle lastMessageAt, TextStyle lastMessageAt,
AvatarTheme avatarTheme, AvatarTheme avatarTheme,
Color unreadCounterColor, Color unreadCounterColor,
double indicatorIconSize,
}) => }) =>
ChannelPreviewTheme( ChannelPreviewTheme(
title: title ?? this.title, title: title ?? this.title,
@@ -534,7 +822,20 @@ class ChannelPreviewTheme {
lastMessageAt: lastMessageAt ?? this.lastMessageAt, lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme, avatarTheme: avatarTheme ?? this.avatarTheme,
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, 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 { class ChannelHeaderTheme {
@@ -562,4 +863,46 @@ class ChannelHeaderTheme {
avatarTheme: avatarTheme ?? this.avatarTheme, avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color, 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,
);
} }
+74 -2
View File
@@ -10,9 +10,9 @@ class StreamSvgIcon extends StatelessWidget {
const StreamSvgIcon({ const StreamSvgIcon({
this.assetName, this.assetName,
this.width,
this.height,
this.color, this.color,
this.width = 24,
this.height = 24,
}); });
@override @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({ factory StreamSvgIcon.penWrite({
double size, double size,
Color color, 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({ factory StreamSvgIcon.edit({
double size, double size,
Color color, Color color,
@@ -757,4 +793,40 @@ class StreamSvgIcon extends StatelessWidget {
height: size, 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,
);
}
} }
+163
View File
@@ -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,
),
],
),
);
}
}
+16 -12
View File
@@ -51,9 +51,13 @@ class UrlAttachment extends StatelessWidget {
), ),
child: Text( child: Text(
hostDisplayName, hostDisplayName,
style: TextStyle( style: StreamChatTheme.of(context)
fontWeight: FontWeight.w700, .textTheme
color: Color(0xFF006CFF), .bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
), ),
), ),
), ),
@@ -61,7 +65,7 @@ class UrlAttachment extends StatelessWidget {
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0), 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(), urlAttachment.title.trim(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: StreamChatTheme.of(context)
fontWeight: FontWeight.w700, .textTheme
fontSize: 12.0, .body
), .copyWith(fontWeight: FontWeight.w700),
), ),
if (urlAttachment.text != null) if (urlAttachment.text != null)
Text( Text(
urlAttachment.text, urlAttachment.text,
style: TextStyle( style: StreamChatTheme.of(context)
fontWeight: FontWeight.w400, .textTheme
fontSize: 12.0, .body
), .copyWith(fontWeight: FontWeight.w400),
), ),
], ],
), ),
+9 -5
View File
@@ -16,7 +16,7 @@ class UserAvatar extends StatelessWidget {
this.borderRadius, this.borderRadius,
this.onlineIndicatorAlignment = Alignment.topRight, this.onlineIndicatorAlignment = Alignment.topRight,
this.selected = false, this.selected = false,
this.selectionColor = const Color(0xFF006CFF), this.selectionColor,
this.selectionThickness = 4, this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
@@ -47,7 +47,7 @@ class UserAvatar extends StatelessWidget {
constraints: constraints ?? constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints, streamChatTheme.ownMessageTheme.avatarTheme.constraints,
decoration: BoxDecoration( decoration: BoxDecoration(
color: streamChatTheme.accentColor, color: streamChatTheme.colorTheme.accentBlue,
), ),
child: hasImage child: hasImage
? CachedNetworkImage( ? CachedNetworkImage(
@@ -61,13 +61,17 @@ class UserAvatar extends StatelessWidget {
: streamChatTheme.defaultUserImage(context, user), : streamChatTheme.defaultUserImage(context, user),
), ),
); );
if (selected) { if (selected) {
avatar = ClipRRect( avatar = ClipRRect(
borderRadius: (borderRadius ?? borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness), BorderRadius.circular(selectionThickness),
child: Container( child: Container(
color: selectionColor, constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
color: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
child: Padding( child: Padding(
padding: EdgeInsets.all(selectionThickness), padding: EdgeInsets.all(selectionThickness),
child: avatar, child: avatar,
@@ -96,10 +100,10 @@ class UserAvatar extends StatelessWidget {
), ),
child: Material( child: Material(
shape: CircleBorder(), shape: CircleBorder(),
color: Color(0xff20E070), color: streamChatTheme.colorTheme.accentGreen,
), ),
), ),
color: Colors.white, color: StreamChatTheme.of(context).colorTheme.white,
), ),
), ),
), ),
+10 -10
View File
@@ -73,25 +73,25 @@ class UserItem extends StatelessWidget {
), ),
), ),
trailing: selected trailing: selected
? CircleAvatar( ? StreamSvgIcon.checkSend(
child: StreamSvgIcon.check( color: StreamChatTheme.of(context).colorTheme.accentBlue,
size: 20,
color: Colors.white,
),
radius: 10,
) )
: null, : null,
title: Text( title: Text(
user.name, user.name,
style: TextStyle(fontWeight: FontWeight.bold), style: StreamChatTheme.of(context).textTheme.bodyBold,
), ),
subtitle: showLastOnline ? _buildLastActive(context) : null, subtitle: showLastOnline ? _buildLastActive(context) : null,
); );
} }
Widget _buildLastActive(context) { Widget _buildLastActive(context) {
return user.online == true return Text(
? Text('Online') user.online == true
: Text('Last online ${Jiffy(user.lastActive).fromNow()}'); ? 'Online'
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
);
} }
} }
+10 -7
View File
@@ -356,15 +356,17 @@ class _UserListViewState extends State<UserListView>
headerItem: (header) { headerItem: (header) {
return Container( return Container(
key: ValueKey<String>('HEADER-$header'), key: ValueKey<String>('HEADER-$header'),
color: Colors.black.withOpacity(0.05), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
child: Text( child: Text(
header, header,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 14.5,
color: Colors.black.withOpacity(0.3)), color: StreamChatTheme.of(context).colorTheme.grey,
),
), ),
), ),
); );
@@ -449,7 +451,10 @@ class _UserListViewState extends State<UserListView>
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container( return Container(
color: Color(0xffd0021B).withAlpha(26), color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center( child: Center(
@@ -471,9 +476,7 @@ class _UserListViewState extends State<UserListView>
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) {
return Container( return Container(
height: 1, height: 1,
color: Theme.of(context).brightness == Brightness.dark color: StreamChatTheme.of(context).colorTheme.greyWhisper,
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
); );
} }
+125 -11
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'stream_svg_icon.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
Future<void> launchURL(BuildContext context, String url) async { Future<void> launchURL(BuildContext context, String url) async {
@@ -25,7 +27,7 @@ Future<bool> showConfirmationDialog(
String cancelText, String cancelText,
}) { }) {
return showModalBottomSheet( return showModalBottomSheet(
backgroundColor: Colors.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
context: context, context: context,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -45,7 +47,7 @@ Future<bool> showConfirmationDialog(
), ),
Text( Text(
title, title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), style: StreamChatTheme.of(context).textTheme.headlineBold,
), ),
SizedBox( SizedBox(
height: 7.0, height: 7.0,
@@ -55,7 +57,8 @@ Future<bool> showConfirmationDialog(
height: 36.0, height: 36.0,
), ),
Container( Container(
color: Color(0xffe6e6e6), color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
height: 1.0, height: 1.0,
), ),
Row( Row(
@@ -65,7 +68,10 @@ Future<bool> showConfirmationDialog(
child: Text( child: Text(
cancelText, cancelText,
style: TextStyle( style: TextStyle(
color: Colors.black.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
fontWeight: FontWeight.w400), fontWeight: FontWeight.w400),
), ),
onPressed: () { onPressed: () {
@@ -93,11 +99,119 @@ Future<bool> showConfirmationDialog(
String getRandomPicUrl(User user) => String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
/// List extension /// Get websiteName from [hostName]
extension ListX<T> on List<T> { String getWebsiteName(String hostName) {
/// Insert any item<T> inBetween the list items switch (hostName) {
List<T> insertBetween(T item) => expand((e) sync* { case 'reddit':
yield item; return 'Reddit';
yield e; case 'youtube':
}).skip(1).toList(growable: false); 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;
}
} }
+3
View File
@@ -14,6 +14,7 @@ class VideoAttachment extends StatefulWidget {
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Size size; final Size size;
final Message message; final Message message;
final ShowMessageCallback onShowMessage;
VideoAttachment({ VideoAttachment({
Key key, Key key,
@@ -21,6 +22,7 @@ class VideoAttachment extends StatefulWidget {
@required this.messageTheme, @required this.messageTheme,
this.message, this.message,
this.size, this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -95,6 +97,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
userName: widget.message.user.name, userName: widget.message.user.name,
sentAt: widget.message.createdAt, sentAt: widget.message.createdAt,
message: widget.message, message: widget.message,
onShowMessage: widget.onShowMessage,
), ),
), ),
), ),
+3
View File
@@ -43,3 +43,6 @@ export 'src/message_search_bloc.dart';
export 'src/message_search_item.dart'; export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart'; export 'src/message_search_list_view.dart';
export 'src/unread_indicator.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';
+5
View File
@@ -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

+3 -3
View File
@@ -24,11 +24,11 @@ dependencies:
url_launcher: ^5.4.11 url_launcher: ^5.4.11
emojis: ^0.9.3 emojis: ^0.9.3
video_player: ^1.0.0 video_player: ^1.0.0
chewie: ^0.10.4 chewie: ^0.12.1
file_picker: ^2.0.12 file_picker: ^2.0.12
image_picker: ^0.6.7+2 image_picker: ^0.6.7+2
flutter_keyboard_visibility: ^3.3.0 flutter_keyboard_visibility: ^4.0.2
stream_chat: ^0.2.20 stream_chat: ^0.2.23
mime: ^0.9.6+3 mime: ^0.9.6+3
video_compress: ^2.1.1 video_compress: ^2.1.1
visibility_detector: ^0.1.5 visibility_detector: ^0.1.5
+2
View File
@@ -71,6 +71,7 @@ void main() {
showCopyMessage: false, showCopyMessage: false,
showDeleteMessage: false, showDeleteMessage: false,
showReply: false, showReply: false,
showThreadReply: false,
message: Message( message: Message(
text: 'test', text: 'test',
user: User( user: User(
@@ -87,6 +88,7 @@ void main() {
await tester.pump(Duration(milliseconds: 1000)); await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.text('Reply'), findsNothing);
expect(find.text('Thread reply'), findsNothing); expect(find.text('Thread reply'), findsNothing);
expect(find.text('Edit message'), findsNothing); expect(find.text('Edit message'), findsNothing);
expect(find.text('Delete message'), findsNothing); expect(find.text('Delete message'), findsNothing);
+1 -1
View File
@@ -74,7 +74,7 @@ void main() {
), ),
latestReactions: [ latestReactions: [
Reaction( Reaction(
type: 'thumbs_up', type: 'like',
user: User(id: testUserId), user: User(id: testUserId),
), ),
Reaction( Reaction(
+3 -3
View File
@@ -31,7 +31,7 @@ void main() {
); );
testWidgets( testWidgets(
'it should show a thumb up', 'it should show a like',
(WidgetTester tester) async { (WidgetTester tester) async {
final client = MockClient(); final client = MockClient();
final clientState = MockClientState(); final clientState = MockClientState();
@@ -50,7 +50,7 @@ void main() {
child: ReactionBubble( child: ReactionBubble(
reactions: [ reactions: [
Reaction( Reaction(
type: 'thumbs_up', type: 'like',
user: User(id: 'test'), user: User(id: 'test'),
), ),
], ],
@@ -86,7 +86,7 @@ void main() {
child: ReactionBubble( child: ReactionBubble(
reactions: [ reactions: [
Reaction( Reaction(
type: 'thumbs_up', type: 'like',
user: User(id: 'test'), user: User(id: 'test'),
), ),
Reaction( Reaction(