Merge branch 'feature/new-ui' into feature/theme

This commit is contained in:
Salvatore Giordano
2020-12-07 16:37:34 +01:00
25 changed files with 2190 additions and 383 deletions
@@ -6,8 +6,8 @@
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:name=".Application" android:name=".Application"
android:label="ChatSample" android:label="ChatSample"
+12
View File
@@ -30,6 +30,8 @@ PODS:
- DKPhotoGallery/Resource (0.0.17): - DKPhotoGallery/Resource (0.0.17):
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
- esys_flutter_share (0.0.1):
- Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
@@ -106,6 +108,8 @@ PODS:
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (6.7.2): - GoogleUtilities/UserDefaults (6.7.2):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- image_gallery_saver (1.5.0):
- Flutter
- image_picker (0.0.1): - image_picker (0.0.1):
- Flutter - Flutter
- nanopb (1.30906.0): - nanopb (1.30906.0):
@@ -159,6 +163,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@@ -168,6 +173,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@@ -203,6 +209,8 @@ SPEC REPOS:
- SwiftyGif - SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
esys_flutter_share:
:path: ".symlinks/plugins/esys_flutter_share/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
firebase_core: firebase_core:
@@ -221,6 +229,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios" :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage: flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
image_picker: image_picker:
:path: ".symlinks/plugins/image_picker/ios" :path: ".symlinks/plugins/image_picker/ios"
path_provider: path_provider:
@@ -245,6 +255,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781 firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781
@@ -263,6 +274,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2
image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
@@ -320,12 +320,14 @@
"${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/esys_flutter_share/esys_flutter_share.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/flutter_apns/flutter_apns.framework", "${BUILT_PRODUCTS_DIR}/flutter_apns/flutter_apns.framework",
"${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework",
"${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework",
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
"${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
@@ -352,12 +354,14 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/esys_flutter_share.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_apns.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_apns.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
+6 -6
View File
@@ -13,41 +13,41 @@ import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY'; const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID'; const kStreamUserId = 'STREAM_USER_ID';
const kStreamToken = 'STREAM_TOKEN'; const kStreamToken = 'STREAM_TOKEN';
const kDefaultStreamApiKey = 's2dxdhpxd94g'; const kDefaultStreamApiKey = 'uj7qrdbfrzvg';
class ChooseUserPage extends StatelessWidget { class ChooseUserPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final users = <String, User>{ final users = <String, User>{
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI':
User( User(
id: 'vishal', id: 'vishal',
extraData: { extraData: {
'name': 'Vishal', 'name': 'Vishal',
}, },
), ),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.1AvvK2TvEsfcxAlfOK2iUMFACucnk6N-Q8f5bbfnjCM':
User( User(
id: 'super-band-9', id: 'super-band-9',
extraData: { extraData: {
'name': 'John Doe', 'name': 'John Doe',
}, },
), ),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.GMEKyEhONmFnqtkf1TR1A3oUOSIWhjfQv5RpI906dAM': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.uQ-pz8e1_C-Df2rIFpkkvWf7g4M0aW2Zkr7pHVhmU1Y':
User( User(
id: 'salvatore', id: 'salvatore',
extraData: { extraData: {
'name': 'Salvatore', 'name': 'Salvatore',
}, },
), ),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GTalMeZHaBdpIM5w-KWrVIDSy-ODkHTRkf1GZbWAveM': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.feLvMytd5_3-eETKrRdwtO3jcA0HxD8woFUbFU7gQCg':
User( User(
id: 'tommaso', id: 'tommaso',
extraData: { extraData: {
'name': 'Tommaso', 'name': 'Tommaso',
}, },
), ),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.xpGE2lu4OoYS7-2yy6PbF0gJnxOaxeGO4EU6xo4EdMU': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.VLkLs6KH-jRRkPvhYMa4_lqN0R6WiK7JhJVmYvxcKz0':
User( User(
id: 'jaap', id: 'jaap',
extraData: { extraData: {
+5
View File
@@ -485,6 +485,11 @@ class _ChannelListViewState extends State<ChannelListView>
onTap: () { onTap: () {
onTap(channel, widget.channelWidget); onTap(channel, widget.channelWidget);
}, },
onLongPress: widget.onChannelLongPress != null
? () {
widget.onChannelLongPress(channel);
}
: null,
), ),
), ),
), ),
+156 -10
View File
@@ -1,32 +1,178 @@
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/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
class FileAttachment extends StatelessWidget { class FileAttachment extends StatelessWidget {
final Attachment attachment; final Attachment attachment;
final Size size; final Size size;
final Widget trailing;
const FileAttachment({ const FileAttachment({
Key key, Key key,
@required this.attachment, @required this.attachment,
this.size, this.size,
this.trailing,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
child: InkWell( child: Container(
onTap: () { width: size?.width ?? 100,
launchURL(context, attachment.assetUrl); height: 56.0,
}, margin: trailing != null ? EdgeInsets.only(top: 4.0) : null,
child: Container( decoration: BoxDecoration(
width: size?.width ?? 100, color: Colors.white,
height: size?.height ?? 100, borderRadius: trailing != null ? BorderRadius.circular(16.0) : null,
child: Center( border: trailing != null
child: Icon(Icons.attach_file), ? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
), : null,
), ),
child: Row(
children: [
Container(
child: _getFileTypeImage(attachment.extraData['mime_type']),
height: 40.0,
width: 33.33,
margin: EdgeInsets.all(8.0),
),
SizedBox(
width: 6.0,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attachment?.title ?? 'File',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 3.0,
),
Text(
'${attachment.extraData['file_size'] ?? 'N/A'} bytes',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 14.0,
),
),
],
),
),
Column(
children: [
trailing ??
IconButton(
icon: StreamSvgIcon.cloud_download(
color: Colors.black,
),
onPressed: () {
launchURL(context, attachment.assetUrl);
},
),
],
),
],
),
// 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);
// },
// ),
// ),
), ),
); );
} }
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;
}
}
} }
-35
View File
@@ -1,35 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
/// A full screen image widget
class FullScreenImage extends StatelessWidget {
/// The url of the image
final String url;
/// Instantiate a new FullScreenImage
const FullScreenImage({
Key key,
@required this.url,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: PhotoView(
imageProvider: CachedNetworkImageProvider(url),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: url,
),
),
);
}
}
+259
View File
@@ -0,0 +1,259 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:video_player/video_player.dart';
import '../stream_chat_flutter.dart';
/// A full screen image widget
class FullScreenMedia extends StatefulWidget {
/// The url of the image
final List<Attachment> mediaAttachments;
final Message message;
final int startIndex;
final String userName;
final DateTime sentAt;
/// Instantiate a new FullScreenImage
const FullScreenMedia({
Key key,
@required this.mediaAttachments,
this.message,
this.startIndex = 0,
this.userName = '',
this.sentAt,
}) : super(key: key);
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
}
class _FullScreenMediaState extends State<FullScreenMedia>
with SingleTickerProviderStateMixin {
bool _optionsShown = true;
AnimationController _controller;
PageController _pageController;
int _currentPage;
List<VideoPackage> videoPackages = [];
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_pageController = PageController(initialPage: widget.startIndex);
_currentPage = widget.startIndex;
widget.mediaAttachments
.where((element) => element.type == 'video')
.toList()
.forEach((element) {
videoPackages.add(VideoPackage(context, element, () {
setState(() {});
}));
});
}
@override
Widget build(BuildContext context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, snapshot) {
return PageView.builder(
controller: _pageController,
onPageChanged: (val) {
setState(() {
_currentPage = val;
});
},
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'image' ||
widget.mediaAttachments[position].type == 'giphy') {
return PhotoView(
imageProvider: CachedNetworkImageProvider(
widget.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments,
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
end: Colors.black)
.lerp(_controller.value),
),
onTapUp: (a, b, c) {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
);
} else if (widget.mediaAttachments[position].type ==
'video') {
var controllerPackage = videoPackages[videoAttachments
.indexOf(widget.mediaAttachments[position])];
if (!controllerPackage.initialised) {
return Center(
child: CircularProgressIndicator(),
);
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50.0,
),
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
}
return Container();
},
itemCount: widget.mediaAttachments.length,
);
}),
AnimatedOpacity(
opacity: _optionsShown ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageHeader(
userName: widget.userName,
sentAt: widget.message.createdAt == null
? ''
: 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}',
onBackPressed: () {
Navigator.of(context).pop();
},
message: widget.message,
urls: widget.mediaAttachments,
currentIndex: _currentPage,
),
ImageFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
videoPackages: videoPackages,
mediaSelectedCallBack: (val) {
setState(() {
_currentPage = val;
_pageController.animateToPage(val,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut);
Navigator.pop(context);
});
},
),
],
),
),
],
),
);
}
String getDay(DateTime dateTime) {
var now = DateTime.now();
if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
DateTime(now.year, now.month, now.day)) {
return 'today';
} else if (DateTime(now.year, now.month, now.day)
.difference(dateTime)
.inHours <
24) {
return 'yesterday';
} else {
return 'on ${Jiffy(dateTime).format("MMM do")}';
}
}
@override
void dispose() {
videoPackages.forEach((element) {
element.dispose();
});
super.dispose();
}
}
class VideoPackage {
VideoPlayerController _videoPlayerController;
ChewieController _chewieController;
bool initialised = false;
VoidCallback onInit;
BuildContext context;
///
VideoPackage(this.context, Attachment attachment, this.onInit) {
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
initialised = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
onInit();
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(context, attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
get videoPlayer => _videoPlayerController;
get chewieController => _chewieController;
void dispose() {
_videoPlayerController.dispose();
_chewieController.dispose();
}
}
-84
View File
@@ -1,84 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'utils.dart';
class FullScreenVideo extends StatefulWidget {
final Attachment attachment;
FullScreenVideo({
Key key,
@required this.attachment,
}) : super(key: key);
@override
_FullScreenVideoState createState() => _FullScreenVideoState();
}
class _FullScreenVideoState extends State<FullScreenVideo> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: Builder(
key: _scaffoldKey,
builder: (context) {
if (!initialized) {
return Center(
child: CircularProgressIndicator(),
);
}
return Chewie(
controller: _chewieController,
);
},
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
});
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(_scaffoldKey.currentContext, widget.attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
+64 -45
View File
@@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'attachment_error.dart'; import 'attachment_error.dart';
import 'full_screen_image.dart'; import 'full_screen_media.dart';
class GiphyAttachment extends StatelessWidget { class GiphyAttachment extends StatelessWidget {
final Attachment attachment; final Attachment attachment;
@@ -64,64 +64,75 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) { Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage( final channel = StreamChannel.of(context).channel;
url: attachment.imageUrl ??
attachment.assetUrl ?? return StreamChannel(
attachment.thumbUrl, channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
); );
})); }));
}, },
child: CachedNetworkImage( child: ClipRRect(
height: size?.height, borderRadius: BorderRadius.only(
width: size?.width, topLeft: Radius.circular(8),
placeholder: (_, __) { topRight: Radius.circular(8),
return Container( ),
width: size?.width, child: CachedNetworkImage(
height: size?.height, height: size?.height,
child: Center( width: size?.width,
child: CircularProgressIndicator(), placeholder: (_, __) {
), return Container(
); width: size?.width,
}, height: size?.height,
imageUrl: attachment.thumbUrl ?? child: Center(
attachment.imageUrl ?? child: CircularProgressIndicator(),
attachment.assetUrl, ),
errorWidget: (context, url, error) => AttachmentError( );
attachment: attachment, },
size: size, imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
), ),
fit: BoxFit.cover,
), ),
), ),
), ),
Positioned( Positioned(
left: 0, bottom: 16,
top: 0, left: 16,
child: Container( child: Material(
decoration: BoxDecoration( color: Colors.black.withOpacity(.5),
color: Colors.white, shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.circular(12),
bottomRight: Radius.circular(16.0), ),
)),
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.symmetric(
left: 8.0, horizontal: 8.0,
right: 8.0, vertical: 4.0,
top: 8.0,
bottom: 4.0,
), ),
child: Row( child: Row(
children: [ children: [
StreamSvgIcon.lightning( StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).accentColor, color: Colors.white,
size: 16.0, size: 16,
), ),
Text( Text(
'GIPHY', 'GIPHY',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).accentColor, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11.0, fontSize: 11,
), ),
), ),
], ],
@@ -278,10 +289,18 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) { Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage( var channel = StreamChannel.of(context).channel;
url: attachment.imageUrl ??
attachment.assetUrl ?? return StreamChannel(
attachment.thumbUrl, channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
); );
})); }));
}, },
+222
View File
@@ -0,0 +1,222 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import '../stream_chat_flutter.dart';
import 'package:path_provider/path_provider.dart';
class ImageActionsModal extends StatelessWidget {
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
ImageActionsModal(
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.pop(context);
},
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.transparent,
],
stops: [0.0, 0.4],
)),
)),
_buildPage(context),
],
),
);
}
Widget _buildPage(context) {
return Material(
color: Colors.transparent,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 40.0,
),
),
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
userName,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
Text(
sentAt,
style: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.copyWith(color: Colors.white.withOpacity(0.5)),
),
],
),
IconButton(
icon: StreamSvgIcon.close(
size: 24.0,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
Align(
alignment: Alignment.centerRight,
child: Container(
width: MediaQuery.of(context).size.width / 1.8,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Color(0xffe5e5e5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: ListTile.divideTiles(
context: context,
tiles: [
_buildButton(
context,
'Reply',
StreamSvgIcon.Icon_curve_line_left_up(
size: 24.0,
color: Colors.black.withOpacity(0.5),
),
() {}),
_buildButton(
context,
'Show in Chat',
StreamSvgIcon.eye(
size: 24.0,
color: Colors.black,
), () {
Navigator.pop(context);
Navigator.pop(context);
}),
_buildButton(
context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.Icon_save(
size: 24.0,
color: Colors.black.withOpacity(0.5),
), () async {
var url = urls[currentIndex].imageUrl ??
urls[currentIndex].assetUrl ??
urls[currentIndex].thumbUrl;
if (urls[currentIndex].type == 'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
}),
if (StreamChat.of(context).user.id == message.user.id)
_buildButton(
context,
'Delete',
StreamSvgIcon.delete(
size: 24.0,
color: Color(0xffFF3742),
),
() {
Navigator.pop(context);
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
color: Color(0xffFF3742),
),
],
).toList(),
),
),
),
),
),
],
),
);
}
Widget _buildButton(
context, String title, StreamSvgIcon icon, VoidCallback onTap,
{Color color}) {
var titleStyle = TextStyle(
fontSize: 14.5,
color: Colors.black,
);
return Material(
child: InkWell(
onTap: onTap,
child: ListTile(
dense: true,
title: Text(
title,
style:
color == null ? titleStyle : titleStyle.copyWith(color: color),
),
leading: icon,
),
),
);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
+13 -5
View File
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'attachment_error.dart'; import 'attachment_error.dart';
import 'attachment_title.dart'; import 'attachment_title.dart';
import 'full_screen_image.dart'; import 'full_screen_media.dart';
import 'utils.dart'; import 'utils.dart';
class ImageAttachment extends StatelessWidget { class ImageAttachment extends StatelessWidget {
@@ -43,10 +43,18 @@ class ImageAttachment extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) { builder: (_) {
return FullScreenImage( final channel = StreamChannel.of(context).channel;
url: attachment.imageUrl ??
attachment.assetUrl ?? return StreamChannel(
attachment.thumbUrl, channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
); );
}, },
), ),
+683
View File
@@ -0,0 +1,683 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_channel.dart';
class ImageFooter extends StatefulWidget {
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// Callback to call when the header is tapped.
final VoidCallback onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback onImageTap;
final int currentPage;
final int totalPages;
final List<Attachment> mediaAttachments;
final Message message;
final List<VideoPackage> videoPackages;
final ValueChanged<int> mediaSelectedCallBack;
/// Creates a channel header
ImageFooter({
Key key,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments,
this.message,
this.videoPackages,
this.mediaSelectedCallBack,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
_ImageFooterState createState() => _ImageFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false;
TextEditingController _searchController;
TextEditingController _messageController = TextEditingController();
String _userNameQuery;
bool _isSearchActive = false;
Set<User> _selectedUsers = {};
bool _loading = false;
Timer _debounce;
Function modalSetStateCallback;
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() {
_userNameQuery = _searchController.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
}
});
}
@override
void initState() {
super.initState();
_searchController = TextEditingController()..addListener(_userNameListener);
}
@override
void dispose() {
_searchController?.clear();
_searchController?.removeListener(_userNameListener);
_searchController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
color:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: StreamSvgIcon.icon_SHARE(
size: 24.0,
color: Colors.black,
),
onPressed: () {
_buildShareModal(context);
},
),
InkWell(
onTap: widget.onTitleTap,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
],
),
),
),
IconButton(
icon: StreamSvgIcon.Icon_grid(
color: Colors.black,
),
onPressed: () {
_buildPhotosModal(context);
},
),
],
),
),
);
}
Widget _buildPhotosModal(context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
)),
child: Stack(
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Photos',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: Colors.black,
),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
),
Container(
color: Colors.white,
child: GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'video') {
var controllerPackage = widget.videoPackages[
videoAttachments
.indexOf(widget.mediaAttachments[position])];
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: FittedBox(
fit: BoxFit.cover,
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
} else {
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: Padding(
padding: const EdgeInsets.all(1.0),
child: AspectRatio(
child: CachedNetworkImage(
imageUrl: widget
.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl,
fit: BoxFit.cover,
),
aspectRatio: 1.0,
),
),
);
}
},
itemCount: widget.mediaAttachments.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3),
),
),
],
);
},
);
}
Widget _buildShareModal(context) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, modalSetState) {
modalSetStateCallback = modalSetState;
return Padding(
padding: EdgeInsets.only(
top: _userSearchMode
? 16.0
: MediaQuery.of(context).size.height / 2,
left: 8.0,
right: 8.0),
child: Material(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
clipBehavior: Clip.antiAlias,
child: Scaffold(
body: UsersBloc(
child: Column(
children: [
_buildTextInputSection(modalSetState),
Expanded(
child: UserListView(
selectedUsers: _selectedUsers,
onUserTap: (user, _) {
_searchController.clear();
if (!_selectedUsers.contains(user)) {
modalSetState(() {
_selectedUsers.add(user);
});
} else {
modalSetState(() {
_selectedUsers.remove(user);
});
}
},
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_searchController.text.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
),
Text(
'No user matches these keywords...'),
],
),
),
),
);
},
);
},
),
),
if (_selectedUsers.isNotEmpty)
_buildShareTextInputSection(modalSetState),
if (!_userSearchMode && _selectedUsers.isEmpty)
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.white,
height: 48.0,
child: Material(
child: InkWell(
onTap: () async {
var url = widget
.mediaAttachments[widget.currentPage]
.imageUrl ??
widget
.mediaAttachments[widget.currentPage]
.assetUrl ??
widget
.mediaAttachments[widget.currentPage]
.thumbUrl;
if (widget
.mediaAttachments[widget.currentPage]
.type ==
'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
},
child: SizedBox.expand(
child: Center(
child: Text(
'Save to Photos',
style: TextStyle(
color: StreamChatTheme.of(context)
.accentColor,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
),
],
),
),
),
),
);
});
},
);
}
Widget _buildTextInputSection(modalSetState) {
if (_userSearchMode) {
return Column(
children: [
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 16.0,
),
Expanded(
child: TextField(
controller: _searchController,
cursorColor: Colors.black,
autofocus: true,
decoration: InputDecoration(
isDense: true,
prefixIconConstraints:
BoxConstraints.tight(Size(36.0, 44.0)),
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0, horizontal: 6.0),
child: StreamSvgIcon.search(
color: Colors.black,
),
),
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide:
BorderSide(color: Colors.black.withOpacity(0.08)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide:
BorderSide(color: Colors.black.withOpacity(0.08)),
),
contentPadding: EdgeInsets.zero,
),
),
),
SizedBox(
width: 8.0,
),
IconButton(
icon: StreamSvgIcon.close_small(
color: Colors.black.withOpacity(0.5),
),
onPressed: () {
modalSetState(() {
_userSearchMode = false;
});
setState(() {});
},
)
],
),
],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.search(
color: Colors.black,
),
iconSize: 24.0,
onPressed: () {
modalSetState(() {
_userSearchMode = true;
});
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Select a Chat to Share',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.share_arrow(
color: Colors.black,
),
onPressed: () async {
var url =
widget.mediaAttachments[widget.currentPage].imageUrl ??
widget.mediaAttachments[widget.currentPage].assetUrl ??
widget.mediaAttachments[widget.currentPage].thumbUrl;
var type =
widget.mediaAttachments[widget.currentPage].type == 'image'
? 'jpg'
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
var request = await HttpClient().getUrl(Uri.parse(url));
var response = await request.close();
var bytes = await consolidateHttpClientResponseBytes(response);
await Share.file('File', 'image.$type', bytes, 'image/$type');
},
),
),
],
);
}
}
Widget _buildShareTextInputSection(modalSetState) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.white,
height: 56.0,
child: _loading
? Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
)
: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: TextField(
controller: _messageController,
onChanged: (val) {
modalSetState(() {});
},
onTap: () {
modalSetState(() {});
setState(() {});
},
decoration: InputDecoration(
isDense: true,
prefixText: ' ',
hintText: 'Add a comment',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.16),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.16),
)),
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
),
),
),
),
IconTheme(
data: StreamChatTheme.of(context)
.channelTheme
.messageInputButtonIconTheme,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
modalSetState(() {
_loading = true;
});
await sendMessage();
modalSetState(() {
_loading = false;
});
},
child: Transform.rotate(
angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context).accentColor,
),
),
),
),
),
),
],
),
),
);
}
/// Sends the current message
Future sendMessage() async {
var text = _messageController.text.trim();
final attachments = widget.message.attachments;
_messageController.clear();
final client = StreamChat.of(context).client;
for (var user in _selectedUsers) {
var c = client.channel('messaging', extraData: {
'members': [
user.id,
StreamChat.of(context).user.id,
],
});
await c.watch();
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await c.sendMessage(message);
}
_selectedUsers.clear();
Navigator.pop(context);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
/// Used for clipping textfield prefix icon
class IconClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var leftX = size.width / 5;
var rightX = 4 * size.width / 5;
var topY = size.height / 5;
var bottomY = 4 * size.height / 5;
final path = Path();
path.moveTo(leftX, topY);
path.lineTo(leftX, bottomY);
path.lineTo(rightX, bottomY);
path.lineTo(rightX, topY);
path.lineTo(leftX, topY);
path.lineTo(0.0, 0.0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
+14 -29
View File
@@ -3,7 +3,8 @@ import 'package:carousel_slider/carousel_options.dart';
import 'package:carousel_slider/carousel_slider.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_image.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageGroup extends StatelessWidget { class ImageGroup extends StatelessWidget {
const ImageGroup({ const ImageGroup({
@@ -107,37 +108,21 @@ class ImageGroup extends StatelessWidget {
BuildContext context, [ BuildContext context, [
int index, int index,
]) { ]) {
final channel = StreamChannel.of(context).channel;
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) => StreamChannel(
return Scaffold( channel: channel,
appBar: AppBar( child: FullScreenMedia(
backgroundColor: Colors.transparent, mediaAttachments: images,
iconTheme: IconThemeData( startIndex: index,
color: Colors.white, userName: message.user.name,
), sentAt: message.createdAt,
), message: message,
backgroundColor: Colors.black, ),
body: SizedBox.expand( ),
child: CarouselSlider(
items: images
.map((image) => FullScreenImage(
url: image.imageUrl ??
image.thumbUrl ??
image.assetUrl,
))
.toList(),
options: CarouselOptions(
initialPage: index ?? 0,
enableInfiniteScroll: false,
viewportFraction: 0.95,
height: MediaQuery.of(context).size.height,
),
),
),
);
},
), ),
); );
} }
+122
View File
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'image_actions_modal.dart';
import 'stream_channel.dart';
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
final bool showBackButton;
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// Callback to call when the header is tapped.
final VoidCallback onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback onImageTap;
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
/// Creates a channel header
ImageHeader({
Key key,
this.message,
this.urls,
this.currentIndex,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.userName = '',
this.sentAt = '',
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? IconButton(
icon: StreamSvgIcon.close(
color: Colors.black,
size: 24.0,
),
onPressed: onBackPressed,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[
IconButton(
icon: StreamSvgIcon.Icon_menu_point_v(
color: Colors.black,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
},
),
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
userName,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
Text(
sentAt,
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
),
),
),
);
}
@override
final Size preferredSize;
void _showMessageActionModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
context: context,
builder: (context) {
return StreamChannel(
channel: channel,
child: ImageActionsModal(
userName: userName,
sentAt: sentAt,
message: message,
urls: urls,
currentIndex: currentIndex,
),
);
});
}
}
+180 -80
View File
@@ -658,6 +658,34 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildFilePickerSection() { Widget _buildFilePickerSection() {
var _attachmentContainsFile =
_attachments.any((element) => element?.attachment?.type == 'file');
Color _getIconColor(int index) {
switch (index) {
case 0:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 1:
return !_attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 2:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 3:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
}
}
return AnimatedContainer( return AnimatedContainer(
duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero, duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
height: _openFilePickerSection ? _filePickerSize : 0, height: _openFilePickerSection ? _filePickerSize : 0,
@@ -671,49 +699,49 @@ class MessageInputState extends State<MessageInput> {
IconButton( IconButton(
iconSize: 24, iconSize: 24,
icon: StreamSvgIcon.pictures( icon: StreamSvgIcon.pictures(
color: _filePickerIndex == 0 color: _getIconColor(0),
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
), ),
onPressed: () { onPressed: _attachmentContainsFile && _attachments.isNotEmpty
setState(() { ? null
_filePickerIndex = 0; : () {
}); setState(() {
}, _filePickerIndex = 0;
});
},
), ),
IconButton( IconButton(
iconSize: 32, iconSize: 32,
icon: StreamSvgIcon.files( icon: StreamSvgIcon.files(
color: _filePickerIndex == 1 color: _getIconColor(1),
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
), ),
onPressed: () { onPressed: !_attachmentContainsFile && _attachments.isNotEmpty
pickFile(DefaultAttachmentTypes.file, false); ? null
}, : () {
pickFile(DefaultAttachmentTypes.file, false);
},
), ),
IconButton( IconButton(
iconSize: 24, iconSize: 24,
icon: StreamSvgIcon.camera( icon: StreamSvgIcon.camera(
color: _filePickerIndex == 2 color: _getIconColor(2),
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
), ),
onPressed: () { onPressed: _attachmentContainsFile && _attachments.isNotEmpty
pickFile(DefaultAttachmentTypes.image, true); ? null
}, : () {
pickFile(DefaultAttachmentTypes.image, true);
},
), ),
IconButton( IconButton(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
iconSize: 24, iconSize: 24,
icon: StreamSvgIcon.record( icon: StreamSvgIcon.record(
color: _filePickerIndex == 3 color: _getIconColor(3),
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
), ),
onPressed: () { onPressed: _attachmentContainsFile && _attachments.isNotEmpty
pickFile(DefaultAttachmentTypes.video, true); ? null
}, : () {
pickFile(DefaultAttachmentTypes.video, true);
},
), ),
], ],
), ),
@@ -770,6 +798,9 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildPickerSection() { Widget _buildPickerSection() {
var _attachmentContainsFile =
_attachments.any((element) => element.attachment.type == 'file');
switch (_filePickerIndex) { switch (_filePickerIndex) {
case 0: case 0:
return FutureBuilder<bool>( return FutureBuilder<bool>(
@@ -782,19 +813,22 @@ class MessageInputState extends State<MessageInput> {
} }
if (snapshot.data) { if (snapshot.data) {
return MediaListView( return IgnorePointer(
selectedIds: _attachments.map((e) => e.id).toList(), ignoring: _attachmentContainsFile,
onSelect: (media) async { child: MediaListView(
if (!_attachments selectedIds: _attachments.map((e) => e.id).toList(),
.any((element) => element.id == media.id)) { onSelect: (media) async {
_addAttachment(media); if (!_attachments
} else { .any((element) => element.id == media.id)) {
setState(() { _addAttachment(media);
_attachments } else {
.removeWhere((element) => element.id == media.id); setState(() {
}); _attachments
} .removeWhere((element) => element.id == media.id);
}, });
}
},
),
); );
} }
@@ -850,7 +884,7 @@ class MessageInputState extends State<MessageInput> {
); );
if (file.size > _kMaxAttachmentSize) { if (file.size > _kMaxAttachmentSize) {
if (medium.type == AssetType.video) { if (medium?.type == AssetType.video) {
final mediaInfo = await CompressVideoService.compressVideo(file.path); final mediaInfo = await CompressVideoService.compressVideo(file.path);
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) { if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
@@ -890,7 +924,7 @@ class MessageInputState extends State<MessageInput> {
..file = file ..file = file
..attachment = Attachment( ..attachment = Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null, localUri: file.path != null ? Uri.parse(file.path) : null,
type: medium.type == AssetType.image ? 'image' : 'video', type: medium?.type == AssetType.image ? 'image' : 'video',
); );
}); });
@@ -1053,7 +1087,7 @@ class MessageInputState extends State<MessageInput> {
offset: rejoin.length, offset: rejoin.length,
), ),
); );
_debounce.cancel();
_mentionsOverlay?.remove(); _mentionsOverlay?.remove();
_mentionsOverlay = null; _mentionsOverlay = null;
}, },
@@ -1201,44 +1235,98 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachments() { Widget _buildAttachments() {
return _attachments.isEmpty return _attachments.isEmpty
? Container() ? Container()
: LimitedBox( : Column(
maxHeight: 104.0, children: [
child: ListView( if (_attachments.any((e) => e.attachment?.type == 'file'))
scrollDirection: Axis.horizontal, LimitedBox(
children: _attachments maxHeight: 73.0,
.map( child: ListView(
(attachment) => Padding( scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(8.0), children: _attachments
child: ClipRRect( .where((e) => e.attachment?.type == 'file')
borderRadius: BorderRadius.circular(10), .map(
child: Stack( (e) => Padding(
children: <Widget>[ padding:
AspectRatio( const EdgeInsets.symmetric(horizontal: 8.0),
aspectRatio: 1.0, child: ClipRRect(
child: Container( borderRadius: BorderRadius.circular(10),
height: 104, clipBehavior: Clip.antiAlias,
width: 104, child: FileAttachment(
child: _buildAttachment(attachment), attachment: e.attachment,
), size: Size(
), MediaQuery.of(context).size.width * 0.55,
_buildRemoveButton(attachment), MediaQuery.of(context).size.height * 0.3,
attachment.uploaded ),
? SizedBox() trailing: Padding(
: Positioned.fill( padding: const EdgeInsets.all(8.0),
child: Center( child: InkWell(
child: Padding( child: CircleAvatar(
padding: const EdgeInsets.all(16.0), backgroundColor:
child: CircularProgressIndicator(), Colors.black.withOpacity(0.2),
maxRadius: 12.0,
child: StreamSvgIcon.close(
color: Colors.white,
), ),
), ),
onTap: () {
setState(() {
_attachments.remove(e);
});
},
), ),
], ),
), ),
), ),
), ),
) )
.toList(), .toList(),
), ),
),
if (_attachments.any((e) => e.attachment?.type != 'file'))
LimitedBox(
maxHeight: 104.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: _attachments
.where((e) => e.attachment?.type != 'file')
.map(
(attachment) => Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias,
child: Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Container(
height: 104,
width: 104,
child: _buildAttachment(attachment),
),
),
_buildRemoveButton(attachment),
attachment.uploaded
? SizedBox()
: Positioned.fill(
child: Center(
child: Padding(
padding:
const EdgeInsets.all(16.0),
child:
CircularProgressIndicator(),
),
),
),
],
),
),
),
)
.toList(),
),
),
],
); );
} }
@@ -1275,9 +1363,9 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachment(_SendingAttachment attachment) { Widget _buildAttachment(_SendingAttachment attachment) {
if (widget.attachmentThumbnailBuilders if (widget.attachmentThumbnailBuilders
?.containsKey(attachment.attachment.type) == ?.containsKey(attachment.attachment?.type) ==
true) { true) {
return widget.attachmentThumbnailBuilders[attachment.attachment.type]( return widget.attachmentThumbnailBuilders[attachment.attachment?.type](
context, context,
attachment, attachment,
); );
@@ -1287,7 +1375,7 @@ class MessageInputState extends State<MessageInput> {
return SizedBox(); return SizedBox();
} }
switch (attachment.attachment.type) { switch (attachment.attachment?.type) {
case 'image': case 'image':
case 'giphy': case 'giphy':
return attachment.file != null return attachment.file != null
@@ -1562,12 +1650,24 @@ class MessageInputState extends State<MessageInput> {
attachmentType = mimeType.type; attachmentType = mimeType.type;
} }
Map<String, dynamic> extraDataMap = {};
if (mimeType?.subtype != null) {
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
}
if (file.size != null) {
extraDataMap['file_size'] = file.size;
}
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final attachment = _SendingAttachment( final attachment = _SendingAttachment(
file: file, file: file,
attachment: Attachment( attachment: Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null, localUri: file.path != null ? Uri.parse(file.path) : null,
type: attachmentType, type: attachmentType,
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
title: file.name ?? 'File',
), ),
); );
+6 -1
View File
@@ -617,7 +617,12 @@ class _MessageListViewState extends State<MessageListView> {
showDeleteMessage: isMyMessage, showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null, borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
attachmentBorderRadiusGeometry: BorderRadius.circular(16), attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
attachmentPadding: const EdgeInsets.all(2), attachmentPadding: const EdgeInsets.all(2),
borderRadiusGeometry: BorderRadius.only( borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
+87 -81
View File
@@ -180,14 +180,13 @@ 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,
), ),
message: message,
); );
}, },
'giphy': (context, message, attachment) { 'giphy': (context, message, attachment) {
return GiphyAttachment( return GiphyAttachment(
attachment: attachment, attachment: attachment,
messageTheme: messageTheme.copyWith( messageTheme: messageTheme,
messageBackgroundColor: Colors.white,
),
message: message, message: message,
size: Size( size: Size(
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
@@ -218,8 +217,8 @@ class _MessageWidgetState extends State<MessageWidget> {
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0 ? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
: 6.0; : 6.0;
final isGiphy = final hasFiles =
widget.message.attachments?.any((element) => element.type == 'giphy') == widget.message.attachments?.any((element) => element.type == 'file') ==
true; true;
return Portal( return Portal(
@@ -294,18 +293,46 @@ class _MessageWidgetState extends State<MessageWidget> {
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
), ),
) )
: Column( : Material(
crossAxisAlignment: clipBehavior: Clip.antiAlias,
CrossAxisAlignment.start, shape: widget.shape ??
mainAxisSize: MainAxisSize.min, RoundedRectangleBorder(
children: <Widget>[ side: isOnlyEmoji
..._parseAttachments(context), ? BorderSide.none
if (widget.message.text : widget.borderSide ??
.trim() BorderSide(
.isNotEmpty && color: Theme.of(context)
!isGiphy) .brightness ==
_buildTextBubble(context), Brightness
], .dark
? Colors.white
.withAlpha(24)
: Colors.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
..._parseAttachments(context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
),
),
), ),
), ),
if (widget.showReactionPickerIndicator) if (widget.showReactionPickerIndicator)
@@ -448,6 +475,10 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
bool get isGiphy =>
widget.message.attachments?.any((element) => element.type == 'giphy') ==
true;
Widget _buildReadIndicator() { Widget _buildReadIndicator() {
var padding = 0.0; var padding = 0.0;
return Stack( return Stack(
@@ -628,46 +659,39 @@ class _MessageWidgetState extends State<MessageWidget> {
[]; [];
} }
Padding wrapAttachmentWidget( Widget wrapAttachmentWidget(
BuildContext context, BuildContext context,
Widget attachmentWidget, { Widget attachmentWidget, {
Attachment attachment, Attachment attachment,
}) { }) {
final attachmentShape = final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return Padding( return GestureDetector(
padding: EdgeInsets.only( onTap: () => retryMessage(context),
bottom: 4, onLongPress: () => onLongPress(context),
), child: Material(
child: GestureDetector( color: _getBackgroundColor(),
onTap: () => retryMessage(context), clipBehavior: Clip.antiAlias,
onLongPress: () => onLongPress(context), shape: attachmentShape,
child: Material( child: Padding(
color: attachment?.type == 'giphy' padding: widget.attachmentPadding,
? Colors.white child: Material(
: _getBackgroundColor(), clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge, shape: attachmentShape,
shape: attachmentShape, type: MaterialType.transparency,
child: Padding( child: Transform(
padding: widget.attachmentPadding, transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: Material( alignment: Alignment.center,
clipBehavior: Clip.hardEdge, child: Column(
shape: attachmentShape, mainAxisSize: MainAxisSize.min,
type: MaterialType.transparency, crossAxisAlignment: CrossAxisAlignment.end,
child: Transform( children: <Widget>[
transform: Matrix4.rotationY(widget.reverse ? pi : 0), getFailedMessageWidget(
alignment: Alignment.center, context,
child: Column( padding: const EdgeInsets.all(8.0),
mainAxisSize: MainAxisSize.min, ),
crossAxisAlignment: CrossAxisAlignment.end, attachmentWidget,
children: <Widget>[ ],
getFailedMessageWidget(
context,
padding: const EdgeInsets.all(8.0),
),
attachmentWidget,
],
),
), ),
), ),
), ),
@@ -797,30 +821,7 @@ class _MessageWidgetState extends State<MessageWidget> {
return SizedBox(); return SizedBox();
} }
Widget _wrapTextInBubble({
BuildContext context,
Widget child,
}) {
return Material(
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24)
: Colors.black.withAlpha(24),
),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
),
color: _getBackgroundColor(),
child: child,
);
}
Widget _buildTextBubble(BuildContext context) { Widget _buildTextBubble(BuildContext context) {
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Widget child = Transform( Widget child = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
@@ -857,13 +858,6 @@ class _MessageWidgetState extends State<MessageWidget> {
], ],
), ),
); );
if (!isOnlyEmoji) {
child = _wrapTextInBubble(
context: context,
child: child,
);
}
return GestureDetector( return GestureDetector(
onTap: () => retryMessage(context), onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context), onLongPress: () => onLongPress(context),
@@ -871,6 +865,10 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
bool get isOnlyEmoji =>
widget.message.text.characters.isNotEmpty &&
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Color _getBackgroundColor() { Color _getBackgroundColor() {
if ((widget.message.status == MessageSendingStatus.FAILED || if ((widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE || widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
@@ -884,6 +882,14 @@ class _MessageWidgetState extends State<MessageWidget> {
return Color(0xFFE9F2FF); return Color(0xFFE9F2FF);
} }
if (isOnlyEmoji) {
return Colors.transparent;
}
if (isGiphy) {
return Colors.transparent;
}
return widget.messageTheme.messageBackgroundColor; return widget.messageTheme.messageBackgroundColor;
} }
+324
View File
@@ -338,6 +338,30 @@ class StreamSvgIcon extends StatelessWidget {
); );
} }
factory StreamSvgIcon.download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.cloud_download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_cloud_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.copy({ factory StreamSvgIcon.copy({
double size, double size,
Color color, Color color,
@@ -397,4 +421,304 @@ class StreamSvgIcon extends StatelessWidget {
height: size, height: size,
); );
} }
factory StreamSvgIcon.Icon_curve_line_left_up({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_curve_line_left_up.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.icon_SHARE({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'icon_SHARE.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_grid({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_grid.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_send_message({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_send_message.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_menu_point_v({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_menu_point_v.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_save({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_save.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.share_arrow({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'share_arrow.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_7z({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_7z.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_csv({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_CSV.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_doc({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOC.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_docx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOCX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_Generic({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_Generic.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_html({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_html.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_md({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_MD.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_odt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ODT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pdf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PDF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_ppt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pptx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPTX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rtf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RTF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_tar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_txt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TXT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xls({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLS.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xlsx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLSX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_zip({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ZIP.svg',
color: color,
width: size,
height: size,
);
}
} }
+14 -4
View File
@@ -1,7 +1,7 @@
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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_video.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@@ -13,11 +13,13 @@ class VideoAttachment extends StatefulWidget {
final Attachment attachment; final Attachment attachment;
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Size size; final Size size;
final Message message;
VideoAttachment({ VideoAttachment({
Key key, Key key,
@required this.attachment, @required this.attachment,
@required this.messageTheme, @required this.messageTheme,
this.message,
this.size, this.size,
}) : super(key: key); }) : super(key: key);
@@ -81,11 +83,19 @@ class _VideoAttachmentState extends State<VideoAttachment> {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => FullScreenVideo( builder: (_) => StreamChannel(
attachment: widget.attachment, channel: channel,
child: FullScreenMedia(
mediaAttachments: [widget.attachment],
userName: widget.message.user.name,
sentAt: widget.message.createdAt,
message: widget.message,
),
), ),
), ),
); );
@@ -100,7 +110,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.none,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Chewie( Chewie(
+3 -1
View File
@@ -11,7 +11,9 @@ export 'src/channels_bloc.dart';
export 'src/date_divider.dart'; export 'src/date_divider.dart';
export 'src/deleted_message.dart'; export 'src/deleted_message.dart';
export 'src/file_attachment.dart'; export 'src/file_attachment.dart';
export 'src/full_screen_video.dart'; export 'src/full_screen_media.dart';
export 'src/image_header.dart';
export 'src/image_footer.dart';
export 'src/giphy_attachment.dart'; export 'src/giphy_attachment.dart';
export 'src/image_attachment.dart'; export 'src/image_attachment.dart';
export 'src/message_input.dart'; export 'src/message_input.dart';
+5
View File
@@ -0,0 +1,5 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4C0.8954 4 0 3.10457 0 2C0 0.89543 0.8954 0 2 0C3.1046 0 4 0.89543 4 2C4 3.10457 3.1046 4 2 4Z" fill="black"/>
<path d="M2 10C0.8954 10 0 9.1046 0 8C0 6.8954 0.8954 6 2 6C3.1046 6 4 6.8954 4 8C4 9.1046 3.1046 10 2 10Z" fill="black"/>
<path d="M2 16C0.8954 16 0 15.1046 0 14C0 12.8954 0.8954 12 2 12C3.1046 12 4 12.8954 4 14C4 15.1046 3.1046 16 2 16Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

+4
View File
@@ -0,0 +1,4 @@
<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 4C11.4477 4 11 4.44771 11 5V14.529L6.78165 10.306C6.37408 9.89799 5.71326 9.89799 5.30568 10.306C4.8981 10.714 4.89811 11.3756 5.30568 11.7836L11.2096 17.694C11.4267 17.9114 11.7157 18.0129 12 17.9987C12.2843 18.0129 12.5733 17.9113 12.7905 17.694L18.6944 11.7836C19.1019 11.3756 19.1019 10.714 18.6944 10.306C18.2868 9.89799 17.626 9.89799 17.2184 10.306L13 14.529V5C13 4.44771 12.5523 4 12 4Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 21C5 20.4477 5.44771 20 6 20H18C18.5523 20 19 20.4477 19 21C19 21.5523 18.5523 22 18 22H6C5.44772 22 5 21.5523 5 21Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

+3
View File
@@ -0,0 +1,3 @@
<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="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12ZM8 11H12V8L16 12L12 16V13H8V11Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

+2
View File
@@ -38,6 +38,8 @@ dependencies:
flutter_slidable: ^0.5.4 flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1 carousel_slider: ^2.2.1
clipboard: ^0.1.2+8 clipboard: ^0.1.2+8
image_gallery_saver: ^1.6.6
esys_flutter_share: ^1.0.2
photo_manager: ^0.5.8 photo_manager: ^0.5.8
transparent_image: ^1.0.0 transparent_image: ^1.0.0
ezanimation: ^0.4.1 ezanimation: ^0.4.1