Release/1.4.0 beta (#318)
* fix: fixed long system messages overflowing * fix: fixed long system messages overflowing * [UI-Kit] Refactor, add support for downloading attachments. Signed-off-by: Sahil Kumar <[email protected]> * fix: header casing * [UI-Kit] Disable attachment download dialog pop using back button Signed-off-by: Sahil Kumar <[email protected]> * stream_chat_flutter fix svg on web * [UI-Kit] Pop attachment download dialog in case of any error. Signed-off-by: Sahil Kumar <[email protected]> * restore svg * feat: add pr title linter * fix test * fix client headers * update system message design * update gh token env var * add scopes * expose systemMessageBuilder in messagelistview * fix(ui): fix button size * add dispatch workflow * add dispatch workflow * update dispatch acton * add runson * fix ref * fix ref * update secret * specify repo * fix: list scroll keyboard behaviour (#304) * fix: Keyboard now closes by clicking on space or scrolling. * fix: Exposed system and normal message tap builders * fmt: dartfmt * feat: Add support for messages filter in `MessageListView` and `MessageListCore` (#303) * [MessageListView, MessageListCore] Add support for message filter Signed-off-by: Sahil Kumar <[email protected]> * [UI-Kit -> Pubspec] Remove `stream_chat_flutter_core` relative import Signed-off-by: Sahil Kumar <[email protected]> * add relative import * add relative import * remove svg for web * remove badge logic on web * style: Update for team lint (#297) * Update for team lint * fix tests * remove pedantic and sort deps * remove jiffy from system message Co-authored-by: Salvatore Giordano <[email protected]> * add section highlighting sample app repo (#307) Co-authored-by: Salvatore Giordano <[email protected]> * add null check to debounce * fix messagelistview loading errors * remove core relative import * fix: fixes message newline issue (#308) * fix: minor * fix: generate image thumbnails and use transparent image as placeholder for cached network images (#310) * update cdn handling * fix image attachment with new cdn * fix(ui): Fix message input permission request crash (#311) Signed-off-by: Sahil Kumar <[email protected]> * fix(llc): update member presence and add skip_push to message (#314) * update member presence * add skip_push to message * fix tests * fix: Fixed shimmer overflow (#316) * fix dialogs * fix(llc): Fix attachment upload state uneven progress. (#315) * feat(llc): Add rate limiter functions. fix(llc): Fix attachment upload state uneven progress. Signed-off-by: Sahil Kumar <[email protected]> * feat(llc): Export `async.dart` and `rate_limit.dart` Signed-off-by: Sahil Kumar <[email protected]> * refactor(llc): Replace our `rate_limit` implementation with the dart port of js `lodash` to better handlw `cancel` and `flush` functions. Signed-off-by: Sahil Kumar <[email protected]> * fix(UI-Kit): Make android example run on real devices Signed-off-by: Sahil Kumar <[email protected]> * fix offline behaviour Co-authored-by: Salvatore Giordano <[email protected]> * feat: Introduce onAttachmentTap on messages attachment (#309) * introdce onAttachmentTap on message * correct indent * sdfsd Co-authored-by: Salvatore Giordano <[email protected]> * feat: added customization options in main widgets (#312) * polish channelpreview customization options * polish channelheader customization options * polish messageinput customization options * polish threadheader customization options * polish channellistheader customization options * show the parent message if no messages in thread * fix edit message * fix channelinfo textstyle * fix review * fix useravatar * add ontitle tap to thread header * add custom message actions * add borderradius and button builder * add ontap to messageinput custom send button * fix bottom sheet * add doc * use placeholder image * add doc * fix listtile density * remove subtitle from ChannelListHeaderTheme.copyWith * add InputDecoration.merge extension * fix: use shimmer in images * fix: remove notification badge logic from the sdk and move it to sample app * fix import * fix: Method being called during build (#317) * method call bug fix * dartfmt * fix: show error messages as system and keep them in the message input (#319) * fix: pre release (#324) * use share_plus plugin * bump dependency versions * fix ui for web * unfocus messageinput only on commands * fix default error for messagesearchlistview * fix thumbnail animation * fix lint * ignore example in linter * update changelogs * fix lint * fix lint * update stream_chat pana min * add doc Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Deven Joshi <[email protected]> Co-authored-by: Neevash Ramdial (Nash) <[email protected]> Co-authored-by: Nelson Nunes <[email protected]>
This commit is contained in:
co-authored by
Sahil Kumar
Deven Joshi
Neevash Ramdial
Nelson Nunes
parent
3332a48a0e
commit
4488fa6c3a
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
@@ -43,10 +44,10 @@ class FileAttachment extends AttachmentWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(context),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
child: _getFileTypeImage(context),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
@@ -102,12 +103,15 @@ class FileAttachment extends AttachmentWidget {
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
placeholder: (_, __) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../full_screen_media.dart';
|
||||
@@ -139,6 +140,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: CircleBorder(),
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
@@ -152,7 +154,6 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
@@ -167,6 +168,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: CircleBorder(),
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
@@ -180,7 +182,6 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -345,11 +346,15 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor:
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -51,22 +52,46 @@ class ImageAttachment extends AttachmentWidget {
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
final imageUrl =
|
||||
var imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
|
||||
if (imageUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
|
||||
var imageUri = Uri.parse(imageUrl);
|
||||
if (imageUri.host == 'stream-io-cdn.com') {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'h': '500',
|
||||
'w': '500',
|
||||
'crop': 'center',
|
||||
'resize': 'crop',
|
||||
});
|
||||
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'height': '500',
|
||||
'width': '500',
|
||||
'fit': 'crop',
|
||||
});
|
||||
}
|
||||
imageUrl = imageUri.toString();
|
||||
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VoidCallback onAttachmentTap;
|
||||
|
||||
const VideoAttachment({
|
||||
Key key,
|
||||
@@ -20,6 +21,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
this.messageTheme,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
@@ -65,25 +67,26 @@ class VideoAttachment extends AttachmentWidget {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
onTap: onAttachmentTap ??
|
||||
() async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
videoWidget,
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
/// Callback to download an attachment asset
|
||||
typedef AttachmentDownloader = Future<String> Function(
|
||||
Attachment attachment, {
|
||||
ProgressCallback progressCallback,
|
||||
});
|
||||
|
||||
/// Widget that shows the options in the gallery view
|
||||
class AttachmentActionsModal extends StatelessWidget {
|
||||
/// The message containing the attachments
|
||||
final Message message;
|
||||
|
||||
/// Current page index
|
||||
final currentIndex;
|
||||
|
||||
/// Callback to show the message
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
/// Callback to download images
|
||||
final AttachmentDownloader imageDownloader;
|
||||
|
||||
/// Callback to provide download files
|
||||
final AttachmentDownloader fileDownloader;
|
||||
|
||||
/// Returns a new [AttachmentActionsModal]
|
||||
const AttachmentActionsModal({
|
||||
this.message,
|
||||
this.currentIndex,
|
||||
this.onShowMessage,
|
||||
this.imageDownloader,
|
||||
this.fileDownloader,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(height: kToolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
'Reply',
|
||||
StreamSvgIcon.iconCurveLineLeftUp(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context, ReturnActionType.reply);
|
||||
},
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.black,
|
||||
),
|
||||
onShowMessage,
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.iconSave(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
final attachment = message.attachments[currentIndex];
|
||||
final isImage = attachment.type == 'image';
|
||||
final saveFile = fileDownloader ?? _downloadAttachment;
|
||||
final saveImage = imageDownloader ?? _downloadAttachment;
|
||||
final downloader = isImage ? saveImage : saveFile;
|
||||
|
||||
final progressNotifier = ValueNotifier<_DownloadProgress>(
|
||||
_DownloadProgress.initial(),
|
||||
);
|
||||
|
||||
downloader(
|
||||
attachment,
|
||||
progressCallback: (received, total) {
|
||||
progressNotifier.value = _DownloadProgress(
|
||||
total,
|
||||
received,
|
||||
);
|
||||
},
|
||||
).catchError((_) {
|
||||
progressNotifier.value = null;
|
||||
});
|
||||
|
||||
// Closing attachment actions modal before opening
|
||||
// attachment download dialog
|
||||
Navigator.pop(context);
|
||||
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
barrierColor: theme.colorTheme.overlay,
|
||||
builder: (context) => _buildDownloadProgressDialog(
|
||||
context,
|
||||
progressNotifier,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (StreamChat.of(context).user.id == message.user.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.accentRed,
|
||||
),
|
||||
() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (message.attachments.length > 1 ||
|
||||
message.text.isNotEmpty) {
|
||||
final remainingAttachments = [...message.attachments]
|
||||
..removeAt(currentIndex);
|
||||
channel.updateMessage(message.copyWith(
|
||||
attachments: remainingAttachments,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
channel.deleteMessage(message).then((value) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
color: theme.colorTheme.accentRed,
|
||||
),
|
||||
]
|
||||
.map<Widget>((e) =>
|
||||
Align(alignment: Alignment.centerRight, child: e))
|
||||
.insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
color: theme.colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
context,
|
||||
String title,
|
||||
StreamSvgIcon icon,
|
||||
VoidCallback onTap, {
|
||||
Color color,
|
||||
}) {
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloadProgressDialog(
|
||||
BuildContext context,
|
||||
ValueNotifier<_DownloadProgress> progressNotifier,
|
||||
) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () => Future.value(false),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: progressNotifier,
|
||||
builder: (_, _DownloadProgress progress, __) {
|
||||
// Pop the dialog in case the progress is null or it's completed.
|
||||
if (progress == null || progress?.toProgressIndicatorValue == 1.0) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
Navigator.of(context).pop,
|
||||
);
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 182,
|
||||
width: 182,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
child: Center(
|
||||
child: progress == null
|
||||
? Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: StreamSvgIcon.error(
|
||||
color: theme.colorTheme.greyGainsboro,
|
||||
),
|
||||
)
|
||||
: progress.toProgressIndicatorValue == 1.0
|
||||
? Container(
|
||||
height: 160,
|
||||
width: 160,
|
||||
child: StreamSvgIcon.check(
|
||||
color: theme.colorTheme.greyGainsboro,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: progress.toProgressIndicatorValue,
|
||||
strokeWidth: 8.0,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
theme.colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'${progress.toPercentage}%',
|
||||
style: theme.textTheme.headline.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback progressCallback,
|
||||
}) async {
|
||||
String filePath;
|
||||
final appDocDir = await getTemporaryDirectory();
|
||||
await Dio().download(
|
||||
attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl,
|
||||
(Headers responseHeaders) {
|
||||
final contentType = responseHeaders[Headers.contentTypeHeader];
|
||||
final mimeType = contentType.first?.split('/')?.last;
|
||||
filePath ??= '${appDocDir.path}/${attachment.id}.$mimeType';
|
||||
return filePath;
|
||||
},
|
||||
onReceiveProgress: progressCallback,
|
||||
);
|
||||
final result = await ImageGallerySaver.saveFile(filePath);
|
||||
return (result as Map)['filePath'];
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadProgress {
|
||||
final int total;
|
||||
final int received;
|
||||
|
||||
const _DownloadProgress(this.total, this.received);
|
||||
|
||||
factory _DownloadProgress.initial() {
|
||||
return _DownloadProgress(double.maxFinite.toInt(), 0);
|
||||
}
|
||||
|
||||
double get toProgressIndicatorValue => received / total;
|
||||
|
||||
int get toPercentage => (received * 100) ~/ total;
|
||||
}
|
||||
@@ -21,28 +21,28 @@ class StreamBackButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: RawMaterialButton(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
if (showUnreads)
|
||||
|
||||
@@ -114,9 +114,9 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: members[index].user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64.0,
|
||||
width: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
onlineIndicatorConstraints:
|
||||
@@ -238,8 +238,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
await channel.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@@ -71,12 +71,12 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -71,6 +71,19 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final bool showConnectionStateTile;
|
||||
|
||||
/// Title widget
|
||||
final Widget title;
|
||||
|
||||
/// Subtitle widget
|
||||
final Widget subtitle;
|
||||
|
||||
/// Leading widget
|
||||
final Widget leading;
|
||||
|
||||
/// AppBar actions
|
||||
/// By default it shows the [ChannelImage]
|
||||
final List<Widget> actions;
|
||||
|
||||
/// Creates a channel header
|
||||
ChannelHeader({
|
||||
Key key,
|
||||
@@ -80,6 +93,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.showTypingIndicator = true,
|
||||
this.onImageTap,
|
||||
this.showConnectionStateTile = false,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@@ -87,6 +104,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
final leadingWidget = leading ??
|
||||
(showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox());
|
||||
|
||||
return ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
@@ -111,26 +136,32 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
leading: leadingWidget,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.color,
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
actions: actions ??
|
||||
<Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
borderRadius: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onImageTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
centerTitle: true,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
@@ -141,20 +172,23 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
title ??
|
||||
ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
ChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle,
|
||||
),
|
||||
subtitle ??
|
||||
ChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -96,7 +96,11 @@ class ChannelImage extends StatelessWidget {
|
||||
initialData: otherMember.user,
|
||||
builder: (context, snapshot) {
|
||||
return UserAvatar(
|
||||
borderRadius: borderRadius,
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
user: snapshot.data ?? otherMember.user,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
@@ -120,7 +124,11 @@ class ChannelImage extends StatelessWidget {
|
||||
.toList();
|
||||
return GroupImage(
|
||||
images: images,
|
||||
borderRadius: borderRadius,
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
|
||||
@@ -57,7 +57,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
.subtitle,
|
||||
);
|
||||
} else {
|
||||
final otherMember = members.firstWhere(
|
||||
@@ -69,10 +69,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
if (otherMember.user.online) {
|
||||
alternativeWidget = Text(
|
||||
'Online',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
alternativeWidget = Text(
|
||||
|
||||
@@ -45,7 +45,7 @@ typedef _TitleBuilder = Widget Function(
|
||||
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
|
||||
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelListHeaderTheme] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Instantiates a ChannelListHeader
|
||||
@@ -57,6 +57,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.onNewChatButtonTap,
|
||||
this.showConnectionStateTile = false,
|
||||
this.preNavigationCallback,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
||||
@@ -76,6 +79,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final VoidCallback preNavigationCallback;
|
||||
|
||||
/// Subtitle widget
|
||||
final Widget subtitle;
|
||||
|
||||
/// Leading widget
|
||||
/// By default it shows the logged in user avatar
|
||||
final Widget leading;
|
||||
|
||||
/// AppBar actions
|
||||
/// By default it shows the new chat button
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
@@ -104,76 +118,85 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.color,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
leading: Center(
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ??
|
||||
(_) {
|
||||
if (preNavigationCallback != null) {
|
||||
preNavigationCallback();
|
||||
}
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color =
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
'svgs/icon_pen_write.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: color,
|
||||
);
|
||||
},
|
||||
leading: leading ??
|
||||
Center(
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ??
|
||||
(_) {
|
||||
if (preNavigationCallback != null) {
|
||||
preNavigationCallback();
|
||||
}
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
borderRadius: StreamChatTheme.of(context)
|
||||
.channelListHeaderTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: StreamChatTheme.of(context)
|
||||
.channelListHeaderTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
],
|
||||
title: Builder(
|
||||
builder: (context) {
|
||||
if (titleBuilder != null) {
|
||||
return titleBuilder(context, status, _client);
|
||||
}
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, _client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
actions: actions ??
|
||||
[
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color = StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
'svgs/icon_pen_write.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: color,
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
],
|
||||
title: Column(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (titleBuilder != null) {
|
||||
return titleBuilder(context, status, _client);
|
||||
}
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, _client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
subtitle ?? Offstage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -202,14 +225,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -222,14 +242,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -239,8 +256,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: Text(
|
||||
'Try Again',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.channelListHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
|
||||
@@ -223,8 +223,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
child: child,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -393,16 +393,17 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 238,
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.expand(
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -468,8 +469,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
child: widget.channelWidget,
|
||||
channel: client,
|
||||
child: widget.channelWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -505,12 +506,12 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: ChannelBottomSheet(
|
||||
onViewInfoTap: () {
|
||||
widget.onViewInfoTap(channel);
|
||||
},
|
||||
),
|
||||
channel: channel,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -553,7 +554,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
onImageTap: widget.onImageTap?.call(channel),
|
||||
onImageTap: () => widget.onImageTap?.call(channel),
|
||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||
),
|
||||
),
|
||||
@@ -591,13 +592,13 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: ChannelName(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -78,12 +78,12 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -32,16 +32,37 @@ class ChannelPreview extends StatelessWidget {
|
||||
/// The function called when the image is tapped
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
/// Widget rendering the title
|
||||
final Widget title;
|
||||
|
||||
/// Widget rendering the subtitle
|
||||
final Widget subtitle;
|
||||
|
||||
/// Widget rendering the leading element, by default it shows the [ChannelImage]
|
||||
final Widget leading;
|
||||
|
||||
/// Widget rendering the trailing element, by default it shows the last message date
|
||||
final Widget trailing;
|
||||
|
||||
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
|
||||
final Widget sendingIndicator;
|
||||
|
||||
ChannelPreview({
|
||||
@required this.channel,
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onImageTap,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.sendingIndicator,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
||||
return StreamBuilder<bool>(
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
@@ -49,6 +70,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
return Opacity(
|
||||
opacity: snapshot.data ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
@@ -62,17 +84,18 @@ class ChannelPreview extends StatelessWidget {
|
||||
onLongPress(channel);
|
||||
}
|
||||
},
|
||||
leading: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
leading: leading ??
|
||||
ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<Member>>(
|
||||
stream: channel.state.membersStream,
|
||||
@@ -93,37 +116,36 @@ class ChannelPreview extends StatelessWidget {
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(child: _buildSubtitle(context)),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage = channel.state.messages.lastWhere(
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
StreamChat.of(context).user.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage,
|
||||
size: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
),
|
||||
_buildDate(context),
|
||||
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
||||
sendingIndicator ??
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage = channel.state.messages.lastWhere(
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
StreamChat.of(context).user.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
),
|
||||
trailing ?? _buildDate(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -176,15 +198,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
' Channel is muted',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -192,10 +206,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
return TypingIndicator(
|
||||
channel: channel,
|
||||
alternativeWidget: _buildLastMessage(context),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
|
||||
),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
@@ -44,3 +45,55 @@ extension PlatformFileX on PlatformFile {
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
extension InputDecorationX on InputDecoration {
|
||||
InputDecoration merge(InputDecoration other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
icon: other?.icon,
|
||||
labelText: other?.labelText,
|
||||
labelStyle: labelStyle?.merge(other.labelStyle) ?? other.labelStyle,
|
||||
helperText: other?.helperText,
|
||||
helperStyle: helperStyle?.merge(other.helperStyle) ?? other.helperStyle,
|
||||
helperMaxLines: other?.helperMaxLines,
|
||||
hintText: other?.hintText,
|
||||
hintStyle: hintStyle?.merge(other.hintStyle) ?? other.hintStyle,
|
||||
hintTextDirection: other?.hintTextDirection,
|
||||
hintMaxLines: other?.hintMaxLines,
|
||||
errorText: other?.errorText,
|
||||
errorStyle: errorStyle?.merge(other.errorStyle) ?? other.errorStyle,
|
||||
errorMaxLines: other?.errorMaxLines,
|
||||
floatingLabelBehavior: other?.floatingLabelBehavior,
|
||||
isCollapsed: other?.isCollapsed,
|
||||
isDense: other?.isDense,
|
||||
contentPadding: other?.contentPadding,
|
||||
prefixIcon: other?.prefixIcon,
|
||||
prefix: other?.prefix,
|
||||
prefixText: other?.prefixText,
|
||||
prefixIconConstraints: other?.prefixIconConstraints,
|
||||
prefixStyle: prefixStyle?.merge(other.prefixStyle) ?? other.prefixStyle,
|
||||
suffixIcon: other?.suffixIcon,
|
||||
suffix: other?.suffix,
|
||||
suffixText: other?.suffixText,
|
||||
suffixStyle: suffixStyle?.merge(other.suffixStyle) ?? other.suffixStyle,
|
||||
suffixIconConstraints: other?.suffixIconConstraints,
|
||||
counter: other?.counter,
|
||||
counterText: other?.counterText,
|
||||
counterStyle:
|
||||
counterStyle?.merge(other.counterStyle) ?? other.counterStyle,
|
||||
filled: other?.filled,
|
||||
fillColor: other?.fillColor,
|
||||
focusColor: other?.focusColor,
|
||||
hoverColor: other?.hoverColor,
|
||||
errorBorder: other?.errorBorder,
|
||||
focusedBorder: other?.focusedBorder,
|
||||
focusedErrorBorder: other?.focusedErrorBorder,
|
||||
disabledBorder: other?.disabledBorder,
|
||||
enabledBorder: other?.enabledBorder,
|
||||
border: other?.border,
|
||||
enabled: other?.enabled,
|
||||
semanticCounterText: other?.semanticCounterText,
|
||||
alignLabelWithHint: other?.alignLabelWithHint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ class VideoPackage {
|
||||
|
||||
ChewieController get chewieController => _chewieController;
|
||||
|
||||
bool get initialized => _videoPlayerController.value.initialized;
|
||||
bool get initialized => _videoPlayerController.value.isInitialized;
|
||||
|
||||
VideoPackage(
|
||||
Attachment attachment, {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
class ImageActionsModal extends StatelessWidget {
|
||||
final Message message;
|
||||
final String userName;
|
||||
final String sentAt;
|
||||
final List<Attachment> urls;
|
||||
final currentIndex;
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
ImageActionsModal(
|
||||
{this.message,
|
||||
this.userName,
|
||||
this.sentAt,
|
||||
this.urls,
|
||||
this.currentIndex,
|
||||
this.onShowMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(height: kToolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
'Reply',
|
||||
StreamSvgIcon.iconCurveLineLeftUp(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context, ReturnActionType.reply);
|
||||
},
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onShowMessage,
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.iconSave(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() async {
|
||||
var url = urls[currentIndex].imageUrl ??
|
||||
urls[currentIndex].assetUrl ??
|
||||
urls[currentIndex].thumbUrl;
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
if (urls[currentIndex].type == 'video') {
|
||||
await _saveVideo(url);
|
||||
} else {
|
||||
await _saveImage(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (StreamChat.of(context).user.id == message.user.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (message.attachments.length > 1 ||
|
||||
message.text.isNotEmpty) {
|
||||
final remainingAttachments = [...message.attachments]
|
||||
..removeAt(currentIndex);
|
||||
channel.updateMessage(message.copyWith(
|
||||
attachments: remainingAttachments,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
channel.deleteMessage(message).then((value) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
]
|
||||
.map<Widget>((e) =>
|
||||
Align(alignment: Alignment.centerRight, child: e))
|
||||
.insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
context,
|
||||
String title,
|
||||
StreamSvgIcon icon,
|
||||
VoidCallback onTap, {
|
||||
Color color,
|
||||
}) {
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -90,27 +91,40 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.iconShare(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
var url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
var type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
await Share.file('File', 'image.$type', bytes, 'image/$type');
|
||||
},
|
||||
),
|
||||
kIsWeb
|
||||
? SizedBox()
|
||||
: IconButton(
|
||||
icon: StreamSvgIcon.iconShare(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
final url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
final type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
final request =
|
||||
await HttpClient().getUrl(Uri.parse(url));
|
||||
final response = await request.close();
|
||||
final bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
final tmpPath = await getTemporaryDirectory();
|
||||
final filePath =
|
||||
'${tmpPath.path}/${attachment.id}.$type';
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(bytes);
|
||||
await Share.shareFiles(
|
||||
[filePath],
|
||||
mimeTypes: [
|
||||
'image/$type',
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onTitleTap,
|
||||
child: Container(
|
||||
@@ -223,13 +237,13 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack(index),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
aspectRatio: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'image_actions_modal.dart';
|
||||
import 'attachment_actions_modal.dart';
|
||||
|
||||
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// True if this header shows the leading back button
|
||||
@@ -110,11 +110,8 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: ImageActionsModal(
|
||||
userName: userName,
|
||||
sentAt: sentAt,
|
||||
child: AttachmentActionsModal(
|
||||
message: message,
|
||||
urls: urls,
|
||||
currentIndex: currentIndex,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
|
||||
@@ -56,6 +56,11 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect(media);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -125,11 +130,6 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
]
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Class describing a message action
|
||||
class MessageAction {
|
||||
/// leading widget
|
||||
final Widget leading;
|
||||
|
||||
/// title widget
|
||||
final Widget title;
|
||||
|
||||
/// callback called on tap
|
||||
final OnMessageTap onTap;
|
||||
|
||||
/// returns a new instance of a [MessageAction]
|
||||
MessageAction({
|
||||
this.leading,
|
||||
this.title,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
@@ -34,6 +35,9 @@ class MessageActionsModal extends StatefulWidget {
|
||||
final ShapeBorder attachmentShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
/// List of custom actions
|
||||
final List<MessageAction> customActions;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@@ -53,6 +57,7 @@ class MessageActionsModal extends StatefulWidget {
|
||||
this.messageShape,
|
||||
this.attachmentShape,
|
||||
this.reverse = false,
|
||||
this.customActions = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -223,6 +228,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
_buildFlagButton(context),
|
||||
if (widget.showDeleteMessage)
|
||||
_buildDeleteButton(context),
|
||||
...widget.customActions.map((action) {
|
||||
return _buildCustomAction(
|
||||
context,
|
||||
action,
|
||||
);
|
||||
})
|
||||
].insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
@@ -248,27 +259,69 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
);
|
||||
}
|
||||
|
||||
InkWell _buildCustomAction(
|
||||
BuildContext context,
|
||||
MessageAction messageAction,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
messageAction.onTap?.call(widget.message);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
messageAction.leading ?? Offstage(),
|
||||
const SizedBox(width: 16),
|
||||
messageAction.title ?? Offstage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFlagDialog() async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
var answer = await showConfirmationDialog(context,
|
||||
title: 'Flag Message',
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
question:
|
||||
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
||||
okText: 'FLAG',
|
||||
cancelText: 'CANCEL');
|
||||
final answer = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Flag Message',
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
question:
|
||||
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
||||
okText: 'FLAG',
|
||||
cancelText: 'CANCEL',
|
||||
);
|
||||
|
||||
if (answer) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
if (answer == true) {
|
||||
try {
|
||||
await client.flagMessage(widget.message.id);
|
||||
_showDismissAlert();
|
||||
await showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: theme.colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The message has been reported to a moderator.',
|
||||
title: 'Message flagged',
|
||||
okText: 'OK',
|
||||
);
|
||||
} catch (err) {
|
||||
if (json.decode(err?.body ?? {})['code'] == 4) {
|
||||
_showDismissAlert();
|
||||
await showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: theme.colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The message has been reported to a moderator.',
|
||||
title: 'Message flagged',
|
||||
okText: 'OK',
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert();
|
||||
}
|
||||
@@ -306,133 +359,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showDismissAlert() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
'Message flagged',
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text('The message has been reported to a moderator.'),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorAlert() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
StreamSvgIcon.error(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text('The operation couldn\'t be completed.'),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.error(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The operation couldn\'t be completed.',
|
||||
title: 'Something went wrong',
|
||||
okText: 'OK',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -637,21 +573,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: widget.editMessageInputBuilder != null
|
||||
? widget.editMessageInputBuilder(context, widget.message)
|
||||
: MessageInput(
|
||||
editMessage: widget.message,
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
),
|
||||
widget.editMessageInputBuilder != null
|
||||
? widget.editMessageInputBuilder(context, widget.message)
|
||||
: MessageInput(
|
||||
editMessage: widget.message,
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
@@ -42,6 +43,15 @@ enum DefaultAttachmentTypes {
|
||||
file,
|
||||
}
|
||||
|
||||
/// Available locations for the sendMessage button relative to the textField
|
||||
enum SendButtonLocation {
|
||||
/// inside the textField
|
||||
inside,
|
||||
|
||||
/// outside the textField
|
||||
outside,
|
||||
}
|
||||
|
||||
const _kMinMediaPickerSize = 360.0;
|
||||
|
||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
@@ -107,6 +117,11 @@ class MessageInput extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.quotedMessage,
|
||||
this.onQuotedMessageCleared,
|
||||
this.sendButtonLocation = SendButtonLocation.outside,
|
||||
this.autofocus = false,
|
||||
this.hideSendAsDm = false,
|
||||
this.idleSendButton,
|
||||
this.activeSendButton,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
@@ -134,6 +149,9 @@ class MessageInput extends StatefulWidget {
|
||||
/// If true the attachments button will not be displayed
|
||||
final bool disableAttachments;
|
||||
|
||||
/// Hide send as dm checkbox
|
||||
final bool hideSendAsDm;
|
||||
|
||||
/// The text controller of the TextField
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
@@ -155,6 +173,18 @@ class MessageInput extends StatefulWidget {
|
||||
///
|
||||
final VoidCallback onQuotedMessageCleared;
|
||||
|
||||
/// The location of the send button
|
||||
final SendButtonLocation sendButtonLocation;
|
||||
|
||||
/// Autofocus property passed to the TextField
|
||||
final bool autofocus;
|
||||
|
||||
/// Send button widget in an idle state
|
||||
final Widget idleSendButton;
|
||||
|
||||
/// Send button widget in an active state
|
||||
final Widget activeSendButton;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -281,7 +311,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildTextField(context),
|
||||
),
|
||||
if (widget.parentMessage != null)
|
||||
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _buildDmCheckbox(),
|
||||
@@ -304,15 +334,16 @@ class MessageInputState extends State<MessageInput> {
|
||||
Flex _buildTextField(BuildContext context) {
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (!_commandEnabled) _buildExpandActionsButton(),
|
||||
if (widget.actionsLocation == ActionsLocation.left)
|
||||
...widget.actions ?? [],
|
||||
_buildTextInput(context),
|
||||
_animateSendButton(context),
|
||||
if (widget.actionsLocation == ActionsLocation.right)
|
||||
...widget.actions ?? [],
|
||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -384,14 +415,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
final sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedCrossFade(
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _buildSendButton(context),
|
||||
secondChild: _buildIdleSendButton(context),
|
||||
firstChild: sendButton,
|
||||
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||
duration:
|
||||
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
|
||||
alignment: Alignment.center,
|
||||
@@ -447,101 +484,139 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
border: Border.all(color: theme.colorTheme.greyGainsboro),
|
||||
borderRadius: theme.messageInputTheme.borderRadius,
|
||||
gradient: _focusNode.hasFocus
|
||||
? theme.messageInputTheme.activeBorderGradient
|
||||
: theme.messageInputTheme.idleBorderGradient,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
child: TextField(
|
||||
key: Key('messageInputText'),
|
||||
enabled: _inputEnabled,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
onSubmitted: (_) => sendMessage(),
|
||||
keyboardType: widget.keyboardType,
|
||||
controller: textEditingController,
|
||||
focusNode: _focusNode,
|
||||
style: theme.textTheme.body,
|
||||
autofocus: false,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: _getHint(),
|
||||
hintStyle: theme.textTheme.body.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: theme.messageInputTheme.borderRadius,
|
||||
color: theme.messageInputTheme.inputBackground,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
child: TextField(
|
||||
key: Key('messageInputText'),
|
||||
enabled: _inputEnabled,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
onSubmitted: (_) => sendMessage(),
|
||||
keyboardType: widget.keyboardType,
|
||||
controller: textEditingController,
|
||||
focusNode: _focusNode,
|
||||
style: theme.messageInputTheme.inputTextStyle,
|
||||
autofocus: widget.autofocus,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: _getInputDecoration(),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
||||
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
||||
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
||||
prefixIcon: _commandEnabled
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.accentBlue,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 4, left: 8),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: _commandEnabled
|
||||
? IconButton(
|
||||
icon: StreamSvgIcon.closeSmall(),
|
||||
splashRadius: 24,
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _commandEnabled = false);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _getInputDecoration() {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final passedDecoration = theme.messageInputTheme.inputDecoration;
|
||||
return InputDecoration(
|
||||
isDense: true,
|
||||
hintText: _getHint(),
|
||||
hintStyle: theme.messageInputTheme.inputTextStyle.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
||||
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
||||
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
||||
prefixIcon: _commandEnabled
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.accentBlue,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 4, left: 8),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: Row(
|
||||
children: [
|
||||
if (_commandEnabled)
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.closeSmall(),
|
||||
splashRadius: 24,
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _commandEnabled = false);
|
||||
},
|
||||
),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
),
|
||||
).merge(passedDecoration);
|
||||
}
|
||||
|
||||
Timer _debounce;
|
||||
|
||||
void _onChanged(BuildContext context, String s) {
|
||||
@@ -621,7 +696,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
.last
|
||||
.contains('@')) {
|
||||
_mentionsOverlay = _buildMentionsOverlayEntry();
|
||||
Overlay.of(context).insert(_mentionsOverlay);
|
||||
if (_mentionsOverlay != null) {
|
||||
Overlay.of(context).insert(_mentionsOverlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +723,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
_commandsOverlay = null;
|
||||
} else {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
if (_commandsOverlay != null) {
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,6 +740,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
?.toList() ??
|
||||
[];
|
||||
|
||||
if (commands.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
final size = renderBox.size;
|
||||
|
||||
@@ -833,6 +916,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -930,7 +1014,19 @@ class MessageInputState extends State<MessageInput> {
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: _buildPickerSection(),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: _attachments.keys.toList(),
|
||||
onAddMoreFilesClick: pickFile,
|
||||
onMediaSelected: (media) {
|
||||
if (_attachments.containsKey(media.id)) {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
} else {
|
||||
_addAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -939,109 +1035,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
case 0:
|
||||
return FutureBuilder<bool>(
|
||||
future: PhotoManager.requestPermission(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data) {
|
||||
if (_attachmentContainsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: _attachments.keys.toList(),
|
||||
onSelect: (media) async {
|
||||
if (!_attachments.containsKey(media.id)) {
|
||||
_addAttachment(media);
|
||||
} else {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
Text(
|
||||
'Please enable access to your photos \nand videos so you can share them with friends.',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 6.0),
|
||||
Center(
|
||||
child: Text(
|
||||
'Allow access to your gallery',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
@@ -1090,81 +1083,81 @@ class MessageInputState extends State<MessageInput> {
|
||||
switch (iconType) {
|
||||
case 'giphy':
|
||||
return CircleAvatar(
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.giphyIcon(
|
||||
size: 24.0,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'ban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.iconUserDelete(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'flag':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.flag(
|
||||
size: 14.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'imgur':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: ClipOval(
|
||||
child: StreamSvgIcon.imgur(
|
||||
size: 24.0,
|
||||
),
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'mute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.mute(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'unban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.userAdd(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'unmute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.volumeUp(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.lightning(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1191,6 +1184,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
})?.toList() ??
|
||||
[];
|
||||
|
||||
if (members.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
final size = renderBox.size;
|
||||
|
||||
@@ -1646,11 +1643,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor:
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1676,8 +1677,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
color: Colors.black26,
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1903,14 +1904,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
if (file == null) return;
|
||||
|
||||
final mimeType = file.path.split('/').last.mimeType;
|
||||
final mimeType = file.name?.mimeType;
|
||||
|
||||
final extraDataMap = <String, dynamic>{};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
}
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
} else {
|
||||
attachmentType = 'file';
|
||||
}
|
||||
@@ -2013,9 +2012,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldUnfocus = _commandEnabled;
|
||||
|
||||
if (_commandEnabled) {
|
||||
text = '/${_chosenCommand.name} ' + text;
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
final attachments = [..._attachments.values];
|
||||
@@ -2082,7 +2082,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
sendingFuture = channel.updateMessage(message);
|
||||
}
|
||||
|
||||
if (!shouldUnfocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
|
||||
return sendingFuture.then((resp) {
|
||||
if (resp.message?.type == 'error') {
|
||||
_parseExistingMessage(message);
|
||||
}
|
||||
if (widget.onMessageSent != null) {
|
||||
widget.onMessageSent(resp.message);
|
||||
}
|
||||
@@ -2140,6 +2147,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
@@ -2150,9 +2160,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2240,3 +2247,114 @@ class Tuple2<T1, T2> {
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
final int filePickerIndex;
|
||||
final bool containsFile;
|
||||
final List<String> selectedMedias;
|
||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||
final void Function(AssetEntity) onMediaSelected;
|
||||
|
||||
const _PickerWidget({
|
||||
Key key,
|
||||
@required this.filePickerIndex,
|
||||
@required this.containsFile,
|
||||
@required this.selectedMedias,
|
||||
@required this.onAddMoreFilesClick,
|
||||
@required this.onMediaSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
__PickerWidgetState createState() => __PickerWidgetState();
|
||||
}
|
||||
|
||||
class __PickerWidgetState extends State<_PickerWidget> {
|
||||
Future<bool> requestPermission;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
requestPermission = PhotoManager.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.filePickerIndex != 0) {
|
||||
return Offstage();
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.data) {
|
||||
if (widget.containsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
Text(
|
||||
'Please enable access to your photos \nand videos so you can share them with friends.',
|
||||
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 6.0),
|
||||
Center(
|
||||
child: Text(
|
||||
'Allow access to your gallery',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,15 @@ typedef ParentMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message,
|
||||
);
|
||||
typedef SystemMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message,
|
||||
);
|
||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||
|
||||
typedef OnMessageSwiped = void Function(Message);
|
||||
typedef OnMessageTap = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
class MessageDetails {
|
||||
@@ -128,14 +133,22 @@ class MessageListView extends StatefulWidget {
|
||||
this.showConnectionStateTile = false,
|
||||
this.loadingBuilder,
|
||||
this.emptyBuilder,
|
||||
this.systemMessageBuilder,
|
||||
this.messageListBuilder,
|
||||
this.errorWidgetBuilder,
|
||||
this.messageFilter,
|
||||
this.customAttachmentBuilders,
|
||||
this.onMessageTap,
|
||||
this.onSystemMessageTap,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
final MessageBuilder messageBuilder;
|
||||
|
||||
/// Function used to build a custom system message widget
|
||||
final SystemMessageBuilder systemMessageBuilder;
|
||||
|
||||
/// Function used to build a custom parent message widget
|
||||
final ParentMessageBuilder parentMessageBuilder;
|
||||
|
||||
@@ -204,10 +217,22 @@ class MessageListView extends StatefulWidget {
|
||||
/// of a connection failure.
|
||||
final ErrorBuilder errorWidgetBuilder;
|
||||
|
||||
/// Predicate used to filter messages
|
||||
final bool Function(Message) messageFilter;
|
||||
|
||||
/// Attachment builders for the default message widget
|
||||
/// Please change this in the [MessageWidget] if you are using a custom implementation
|
||||
final Map<String, AttachmentBuilder> customAttachmentBuilders;
|
||||
|
||||
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead)
|
||||
final OnMessageTap onMessageTap;
|
||||
|
||||
/// Called when system message is tapped
|
||||
final OnMessageTap onSystemMessageTap;
|
||||
|
||||
// Customize onTap on attachment
|
||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -265,6 +290,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MessageListCore(
|
||||
messageFilter: widget.messageFilter,
|
||||
loadingBuilder: widget.loadingBuilder ??
|
||||
(context) {
|
||||
return Center(
|
||||
@@ -356,6 +382,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
childAnchor: Alignment.topCenter,
|
||||
message: statusString,
|
||||
child: LazyLoadScrollView(
|
||||
onPageScrollStart: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
@@ -408,7 +437,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
.subtitle,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -595,9 +624,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
@@ -615,6 +641,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.down(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
if (showUnreadCount)
|
||||
Positioned(
|
||||
@@ -804,6 +833,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
},
|
||||
customAttachmentBuilders: widget.customAttachmentBuilders,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onMessageTap != null) {
|
||||
widget.onMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -812,11 +847,19 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
List<Message> messages,
|
||||
int index,
|
||||
) {
|
||||
if (message.type == 'system' && message.text?.isNotEmpty == true) {
|
||||
return SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
);
|
||||
if ((message.type == 'system' || message.type == 'error') &&
|
||||
message.text?.isNotEmpty == true) {
|
||||
return widget.systemMessageBuilder?.call(context, message) ??
|
||||
SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onSystemMessageTap != null) {
|
||||
widget.onSystemMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final userId = StreamChat.of(context).user.id;
|
||||
@@ -965,6 +1008,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
},
|
||||
customAttachmentBuilders: widget.customAttachmentBuilders,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onMessageTap != null) {
|
||||
widget.onMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
onAttachmentTap: widget.onAttachmentTap,
|
||||
);
|
||||
|
||||
if (!message.isDeleted &&
|
||||
@@ -995,10 +1045,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
end: colorTheme.white.withOpacity(0),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: child,
|
||||
),
|
||||
onEnd: () => initialMessageHighlightComplete = true,
|
||||
builder: (_, color, child) {
|
||||
return Container(
|
||||
@@ -1006,6 +1052,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
|
||||
@@ -237,6 +237,9 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? -3 : null,
|
||||
right: isCurrentUser ? -3 : null,
|
||||
child: Align(
|
||||
alignment:
|
||||
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||
@@ -250,9 +253,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
highlightOwnReactions: false,
|
||||
),
|
||||
),
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? -3 : null,
|
||||
right: isCurrentUser ? -3 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -157,53 +157,14 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
errorBuilder: widget.errorBuilder ??
|
||||
(BuildContext context, dynamic error) {
|
||||
if (error is Error) {
|
||||
print((error).stackTrace);
|
||||
}
|
||||
|
||||
var message = error.toString();
|
||||
if (error is DioError) {
|
||||
if (error.type == DioErrorType.RESPONSE) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
print(error.stackTrace);
|
||||
}
|
||||
return InfoTile(
|
||||
showMessage: widget.showErrorTile,
|
||||
tileAnchor: Alignment.topCenter,
|
||||
childAnchor: Alignment.topCenter,
|
||||
message: 'An error occurred.',
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 2.0),
|
||||
child: Icon(Icons.error_outline),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'Error loading messages'),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(message),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_messageSearchListController.loadData();
|
||||
},
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Container(),
|
||||
);
|
||||
},
|
||||
loadingBuilder: widget.loadingBuilder ??
|
||||
|
||||
@@ -21,7 +21,8 @@ class MessageText extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = _replaceMentions(message.text);
|
||||
final text = _replaceMentions(message.text).replaceAll('\n', '\\\n');
|
||||
|
||||
return MarkdownBody(
|
||||
data: text,
|
||||
onTapLink: (
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
@@ -143,6 +144,15 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
/// Function called when message is tapped
|
||||
final void Function(Message) onMessageTap;
|
||||
|
||||
/// List of custom actions shown on message long tap
|
||||
final List<MessageAction> customActions;
|
||||
|
||||
// Customize onTap on attachment
|
||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -157,6 +167,7 @@ class MessageWidget extends StatefulWidget {
|
||||
this.borderRadiusGeometry,
|
||||
this.attachmentBorderRadiusGeometry,
|
||||
this.onMentionTap,
|
||||
this.onMessageTap,
|
||||
this.showReactionPickerIndicator = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = true,
|
||||
@@ -191,6 +202,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
this.customActions = const [],
|
||||
this.onAttachmentTap,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -203,6 +216,11 @@ class MessageWidget extends StatefulWidget {
|
||||
),
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap?.call(message, attachment);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
'video': (context, message, attachment) {
|
||||
@@ -216,6 +234,11 @@ class MessageWidget extends StatefulWidget {
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap?.call(message, attachment);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
'giphy': (context, message, attachment) {
|
||||
@@ -311,6 +334,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
type: MaterialType.transparency,
|
||||
child: Portal(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onMessageTap(widget.message);
|
||||
},
|
||||
onLongPress: widget.message.isDeleted && !isFailedState
|
||||
? null
|
||||
: () => onLongPress(context),
|
||||
@@ -351,9 +377,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-12, 0, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
child: _buildReactionIndicator(context),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
@@ -745,6 +771,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -951,6 +978,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
user: widget.message.user,
|
||||
onTap: widget.onUserAvatarTap,
|
||||
constraints: widget.messageTheme.avatarTheme.constraints,
|
||||
borderRadius: widget.messageTheme.avatarTheme.borderRadius,
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -55,7 +55,7 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
return Container(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.initialized
|
||||
child: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
@@ -94,6 +94,19 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions[ownReactionIndex],
|
||||
);
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animations[index],
|
||||
builder: (context, val) {
|
||||
@@ -121,19 +134,6 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
),
|
||||
);
|
||||
}),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions[ownReactionIndex],
|
||||
);
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app_badger/flutter_app_badger.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -93,9 +92,9 @@ class StreamChatState extends State<StreamChat> {
|
||||
),
|
||||
child: StreamChatCore(
|
||||
client: client,
|
||||
child: widget.child,
|
||||
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
||||
backgroundKeepAlive: widget.backgroundKeepAlive,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -121,12 +120,5 @@ class StreamChatState extends State<StreamChat> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
client.state?.totalUnreadCountStream?.listen((count) {
|
||||
if (count > 0) {
|
||||
FlutterAppBadger.updateBadgeCount(count);
|
||||
} else {
|
||||
FlutterAppBadger.removeBadge();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:stream_chat_flutter/src/channel_preview.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
||||
@@ -45,12 +46,15 @@ class StreamChatThemeData {
|
||||
/// The text themes used in the widgets
|
||||
final TextTheme textTheme;
|
||||
|
||||
/// The text themes used in the widgets
|
||||
/// The color themes used in the widgets
|
||||
final ColorTheme colorTheme;
|
||||
|
||||
/// Theme of the [ChannelPreview]
|
||||
final ChannelPreviewTheme channelPreviewTheme;
|
||||
|
||||
/// Theme of the [ChannelListHeader]
|
||||
final ChannelListHeaderTheme channelListHeaderTheme;
|
||||
|
||||
/// Theme of the chat widgets dedicated to a channel
|
||||
final ChannelTheme channelTheme;
|
||||
|
||||
@@ -60,7 +64,7 @@ class StreamChatThemeData {
|
||||
/// Theme of other users messages
|
||||
final MessageTheme otherMessageTheme;
|
||||
|
||||
/// Theme of other users messages
|
||||
/// Theme dedicated to the [MessageInput] widget
|
||||
final MessageInputTheme messageInputTheme;
|
||||
|
||||
/// The widget that will be built when the channel image is unavailable
|
||||
@@ -79,6 +83,7 @@ class StreamChatThemeData {
|
||||
const StreamChatThemeData({
|
||||
this.textTheme,
|
||||
this.colorTheme,
|
||||
this.channelListHeaderTheme,
|
||||
this.channelPreviewTheme,
|
||||
this.channelTheme,
|
||||
this.otherMessageTheme,
|
||||
@@ -98,9 +103,7 @@ class StreamChatThemeData {
|
||||
accentBlue: theme.accentColor,
|
||||
),
|
||||
defaultTheme.textTheme,
|
||||
).copyWith(
|
||||
// primaryIconTheme: theme.primaryIconTheme,
|
||||
);
|
||||
);
|
||||
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
|
||||
}
|
||||
|
||||
@@ -116,9 +119,12 @@ class StreamChatThemeData {
|
||||
Widget Function(BuildContext, Channel) defaultChannelImage,
|
||||
Widget Function(BuildContext, User) defaultUserImage,
|
||||
IconThemeData primaryIconTheme,
|
||||
ChannelListHeaderTheme channelListHeaderTheme,
|
||||
List<ReactionIcon> reactionIcons,
|
||||
}) =>
|
||||
StreamChatThemeData(
|
||||
channelListHeaderTheme:
|
||||
channelListHeaderTheme ?? this.channelListHeaderTheme,
|
||||
textTheme: textTheme ?? this.textTheme,
|
||||
colorTheme: colorTheme ?? this.colorTheme,
|
||||
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||
@@ -135,6 +141,9 @@ class StreamChatThemeData {
|
||||
StreamChatThemeData merge(StreamChatThemeData other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
channelListHeaderTheme:
|
||||
channelListHeaderTheme?.merge(other.channelListHeaderTheme) ??
|
||||
other.channelListHeaderTheme,
|
||||
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
|
||||
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
|
||||
primaryIconTheme: other.primaryIconTheme,
|
||||
@@ -189,6 +198,17 @@ class StreamChatThemeData {
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
),
|
||||
indicatorIconSize: 16.0),
|
||||
channelListHeaderTheme: ChannelListHeaderTheme(
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
color: colorTheme.white,
|
||||
title: textTheme.headlineBold,
|
||||
),
|
||||
channelTheme: ChannelTheme(
|
||||
channelHeaderTheme: ChannelHeaderTheme(
|
||||
avatarTheme: AvatarTheme(
|
||||
@@ -200,9 +220,8 @@ class StreamChatThemeData {
|
||||
),
|
||||
color: colorTheme.white,
|
||||
title: textTheme.headlineBold,
|
||||
lastMessageAt: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
subtitle: textTheme.footnote.copyWith(
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -249,12 +268,26 @@ class StreamChatThemeData {
|
||||
),
|
||||
),
|
||||
messageInputTheme: MessageInputTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
sendAnimationDuration: Duration(milliseconds: 300),
|
||||
actionButtonColor: colorTheme.accentBlue,
|
||||
actionButtonIdleColor: colorTheme.grey,
|
||||
sendButtonColor: colorTheme.accentBlue,
|
||||
sendButtonIdleColor: colorTheme.greyGainsboro,
|
||||
inputBackground: colorTheme.white,
|
||||
inputTextStyle: textTheme.body,
|
||||
idleBorderGradient: LinearGradient(
|
||||
colors: [
|
||||
colorTheme.greyGainsboro,
|
||||
colorTheme.greyGainsboro,
|
||||
],
|
||||
),
|
||||
activeBorderGradient: LinearGradient(
|
||||
colors: [
|
||||
colorTheme.greyGainsboro,
|
||||
colorTheme.greyGainsboro,
|
||||
],
|
||||
),
|
||||
),
|
||||
reactionIcons: [
|
||||
ReactionIcon(
|
||||
@@ -809,26 +842,26 @@ class ChannelPreviewTheme {
|
||||
|
||||
class ChannelHeaderTheme {
|
||||
final TextStyle title;
|
||||
final TextStyle lastMessageAt;
|
||||
final TextStyle subtitle;
|
||||
final AvatarTheme avatarTheme;
|
||||
final Color color;
|
||||
|
||||
const ChannelHeaderTheme({
|
||||
this.title,
|
||||
this.lastMessageAt,
|
||||
this.subtitle,
|
||||
this.avatarTheme,
|
||||
this.color,
|
||||
});
|
||||
|
||||
ChannelHeaderTheme copyWith({
|
||||
TextStyle title,
|
||||
TextStyle lastMessageAt,
|
||||
TextStyle subtitle,
|
||||
AvatarTheme avatarTheme,
|
||||
Color color,
|
||||
}) =>
|
||||
ChannelHeaderTheme(
|
||||
title: title ?? this.title,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
@@ -837,8 +870,48 @@ class ChannelHeaderTheme {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
lastMessageAt:
|
||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
||||
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
color: other.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme dedicated to the [ChannelListHeader]
|
||||
class ChannelListHeaderTheme {
|
||||
/// Style of the title text
|
||||
final TextStyle title;
|
||||
|
||||
/// Theme dedicated to the userAvatar
|
||||
final AvatarTheme avatarTheme;
|
||||
|
||||
/// Background color of the appbar
|
||||
final Color color;
|
||||
|
||||
/// Returns a new [ChannelListHeaderTheme]
|
||||
const ChannelListHeaderTheme({
|
||||
this.title,
|
||||
this.avatarTheme,
|
||||
this.color,
|
||||
});
|
||||
|
||||
/// Returns a new [ChannelListHeaderTheme] replacing some of its properties
|
||||
ChannelListHeaderTheme copyWith({
|
||||
TextStyle title,
|
||||
AvatarTheme avatarTheme,
|
||||
Color color,
|
||||
}) =>
|
||||
ChannelListHeaderTheme(
|
||||
title: title ?? this.title,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
/// Merges [this] [ChannelListHeaderTheme] with the [other]
|
||||
ChannelListHeaderTheme merge(ChannelListHeaderTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
color: other.color,
|
||||
);
|
||||
@@ -865,6 +938,21 @@ class MessageInputTheme {
|
||||
/// Background color of [MessageInput]
|
||||
final Color inputBackground;
|
||||
|
||||
/// TextStyle of [MessageInput]
|
||||
final TextStyle inputTextStyle;
|
||||
|
||||
/// InputDecoration of [MessageInput]
|
||||
final InputDecoration inputDecoration;
|
||||
|
||||
/// Border gradient when the [MessageInput] is not focused
|
||||
final Gradient idleBorderGradient;
|
||||
|
||||
/// Border gradient when the [MessageInput] is focused
|
||||
final Gradient activeBorderGradient;
|
||||
|
||||
/// Border radius of [MessageInput]
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// Returns a new [MessageInputTheme]
|
||||
const MessageInputTheme({
|
||||
this.sendAnimationDuration,
|
||||
@@ -873,6 +961,11 @@ class MessageInputTheme {
|
||||
this.actionButtonIdleColor,
|
||||
this.sendButtonIdleColor,
|
||||
this.inputBackground,
|
||||
this.inputTextStyle,
|
||||
this.inputDecoration,
|
||||
this.activeBorderGradient,
|
||||
this.idleBorderGradient,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
/// Returns a new [MessageInputTheme] replacing some of its properties
|
||||
@@ -883,6 +976,11 @@ class MessageInputTheme {
|
||||
Color sendButtonColor,
|
||||
Color actionButtonIdleColor,
|
||||
Color sendButtonIdleColor,
|
||||
TextStyle inputTextStyle,
|
||||
InputDecoration inputDecoration,
|
||||
Gradient activeBorderGradient,
|
||||
Gradient idleBorderGradient,
|
||||
BorderRadius borderRadius,
|
||||
}) =>
|
||||
MessageInputTheme(
|
||||
sendAnimationDuration:
|
||||
@@ -892,7 +990,12 @@ class MessageInputTheme {
|
||||
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
|
||||
actionButtonIdleColor:
|
||||
actionButtonIdleColor ?? this.actionButtonIdleColor,
|
||||
inputTextStyle: inputTextStyle ?? this.inputTextStyle,
|
||||
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
|
||||
inputDecoration: inputDecoration ?? this.inputDecoration,
|
||||
activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient,
|
||||
idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
);
|
||||
|
||||
/// Merges [this] [MessageInputTheme] with the [other]
|
||||
@@ -905,6 +1008,12 @@ class MessageInputTheme {
|
||||
actionButtonIdleColor: other.actionButtonIdleColor,
|
||||
sendButtonColor: other.sendButtonColor,
|
||||
sendButtonIdleColor: other.sendButtonIdleColor,
|
||||
inputTextStyle: other.inputTextStyle,
|
||||
inputDecoration: inputDecoration?.merge(other.inputDecoration) ??
|
||||
other.inputDecoration,
|
||||
activeBorderGradient: other.activeBorderGradient,
|
||||
idleBorderGradient: other.idleBorderGradient,
|
||||
borderRadius: other.borderRadius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class StreamNeumorphicButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: child,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
height: 40,
|
||||
width: 40,
|
||||
@@ -35,6 +34,7 @@ class StreamNeumorphicButton extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,24 +18,15 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = Key('StreamSvgIcon-$assetName');
|
||||
return kIsWeb
|
||||
? Image.network(
|
||||
'packages/stream_chat_flutter/svgs/$assetName',
|
||||
width: width,
|
||||
height: height,
|
||||
key: key,
|
||||
color: color,
|
||||
alignment: Alignment.center,
|
||||
)
|
||||
: SvgPicture.asset(
|
||||
'lib/svgs/$assetName',
|
||||
package: 'stream_chat_flutter',
|
||||
key: key,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
return SvgPicture.asset(
|
||||
'lib/svgs/$assetName',
|
||||
package: 'stream_chat_flutter',
|
||||
key: key,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.settings({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
@@ -18,37 +17,7 @@ class SystemMessage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final divider = Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Divider(),
|
||||
),
|
||||
);
|
||||
|
||||
final createdAt = Jiffy(message.createdAt.toLocal());
|
||||
final now = DateTime.now();
|
||||
final hourInfo = createdAt.format('h:mm a');
|
||||
|
||||
String dayInfo;
|
||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||
dayInfo = 'TODAY';
|
||||
} else if (Jiffy(createdAt)
|
||||
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
|
||||
dayInfo = 'YESTERDAY';
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
now.subtract(Duration(days: 7)),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('EEEE').toUpperCase();
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
Jiffy(now).subtract(years: 1),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('dd/MM').toUpperCase();
|
||||
} else {
|
||||
dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
|
||||
}
|
||||
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
@@ -56,59 +25,12 @@ class SystemMessage extends StatelessWidget {
|
||||
onMessageTap(message);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
divider,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
message.text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: dayInfo,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: ' AT'),
|
||||
TextSpan(text: ' $hourInfo'),
|
||||
],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
divider,
|
||||
],
|
||||
child: Text(
|
||||
message.text,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
style: theme.textTheme.captionBold.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,15 +63,35 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// Callback to call when the title is tapped.
|
||||
final VoidCallback onTitleTap;
|
||||
|
||||
/// The message parent of this thread
|
||||
final Message parent;
|
||||
|
||||
/// Title widget
|
||||
final Widget title;
|
||||
|
||||
/// Subtitle widget
|
||||
final Widget subtitle;
|
||||
|
||||
/// Leading widget
|
||||
final Widget leading;
|
||||
|
||||
/// AppBar actions
|
||||
final List<Widget> actions;
|
||||
|
||||
/// Instantiate a new ThreadHeader
|
||||
ThreadHeader({
|
||||
Key key,
|
||||
@required this.parent,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.onTitleTap,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@@ -81,51 +101,61 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
automaticallyImplyLeading: false,
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
cid: StreamChannel.of(context).channel.cid,
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
leading: leading ??
|
||||
(showBackButton
|
||||
? StreamBackButton(
|
||||
cid: StreamChannel.of(context).channel.cid,
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox()),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Thread Reply',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
actions: actions,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
child: Container(
|
||||
height: preferredSize.height,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
title ??
|
||||
Text(
|
||||
'Thread Reply',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
subtitle ??
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.subtitle,
|
||||
),
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.subtitle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,41 +14,43 @@ class UnreadIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return StreamBuilder<int>(
|
||||
stream: cid != null
|
||||
? client.state.channels[cid].state.unreadCountStream
|
||||
: client.state.totalUnreadCountStream,
|
||||
initialData: cid != null
|
||||
? client.state.channels[cid].state.unreadCount
|
||||
: client.state.totalUnreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.unreadCounterColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5.0,
|
||||
right: 5.0,
|
||||
top: 2,
|
||||
bottom: 1,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${snapshot.data}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
return IgnorePointer(
|
||||
child: StreamBuilder<int>(
|
||||
stream: cid != null
|
||||
? client.state.channels[cid].state.unreadCountStream
|
||||
: client.state.totalUnreadCountStream,
|
||||
initialData: cid != null
|
||||
? client.state.channels[cid].state.unreadCount
|
||||
: client.state.totalUnreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.unreadCounterColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5.0,
|
||||
right: 5.0,
|
||||
top: 2,
|
||||
bottom: 1,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${snapshot.data}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ class UrlAttachment extends StatelessWidget {
|
||||
Container(
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
@@ -39,6 +42,12 @@ class UrlAttachment extends StatelessWidget {
|
||||
left: 0.0,
|
||||
bottom: -1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
color: StreamChatTheme.of(context).colorTheme.blueAlice,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
@@ -57,19 +66,10 @@ class UrlAttachment extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
color: StreamChatTheme.of(context).colorTheme.blueAlice,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: textPadding,
|
||||
|
||||
@@ -39,26 +39,29 @@ class UserAvatar extends StatelessWidget {
|
||||
user.extraData['image'] != '';
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
Widget avatar = ClipRRect(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: streamChatTheme.colorTheme.accentBlue,
|
||||
Widget avatar = FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: ClipRRect(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: streamChatTheme.colorTheme.accentBlue,
|
||||
),
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: user.extraData['image'],
|
||||
errorWidget: (_, __, ___) {
|
||||
return streamChatTheme.defaultUserImage(context, user);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: user.extraData['image'],
|
||||
errorWidget: (_, __, ___) {
|
||||
return streamChatTheme.defaultUserImage(context, user);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -91,6 +94,7 @@ class UserAvatar extends StatelessWidget {
|
||||
alignment: onlineIndicatorAlignment,
|
||||
child: Material(
|
||||
type: MaterialType.circle,
|
||||
color: streamChatTheme.colorTheme.white,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2.0),
|
||||
constraints: onlineIndicatorConstraints ??
|
||||
@@ -103,7 +107,6 @@ class UserAvatar extends StatelessWidget {
|
||||
color: streamChatTheme.colorTheme.accentGreen,
|
||||
),
|
||||
),
|
||||
color: streamChatTheme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -36,63 +36,74 @@ Future<bool> showConfirmationDialog(
|
||||
)),
|
||||
builder: (context) {
|
||||
final effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 26.0),
|
||||
if (icon != null) icon,
|
||||
SizedBox(height: 26.0),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(height: 7.0),
|
||||
Text(
|
||||
question,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 36.0),
|
||||
Container(
|
||||
color: effect.color.withOpacity(effect.alpha ?? 1),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 26.0),
|
||||
if (icon != null) icon,
|
||||
SizedBox(height: 26.0),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(height: 7.0),
|
||||
Text(
|
||||
question,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 36.0),
|
||||
Container(
|
||||
color: effect.color.withOpacity(effect.alpha ?? 1),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
okText,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed),
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(
|
||||
okText,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -101,13 +112,13 @@ Future<bool> showInfoDialog(
|
||||
BuildContext context, {
|
||||
String title,
|
||||
Widget icon,
|
||||
String question,
|
||||
String details,
|
||||
String okText,
|
||||
StreamChatThemeData theme,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
backgroundColor:
|
||||
theme.colorTheme.white ?? StreamChatTheme.of(context).colorTheme.white,
|
||||
backgroundColor: theme?.colorTheme?.white ??
|
||||
StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
@@ -115,54 +126,51 @@ Future<bool> showInfoDialog(
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
if (icon != null) icon,
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.headlineBold ??
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text(question),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color: theme.colorTheme.black.withOpacity(.08) ??
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
okText,
|
||||
style: TextStyle(
|
||||
color: theme.colorTheme.black.withOpacity(0.5) ??
|
||||
StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
if (icon != null) icon,
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: theme?.textTheme?.headlineBold ??
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text(details),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color: theme?.colorTheme?.black?.withOpacity(.08) ??
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
okText,
|
||||
style: TextStyle(
|
||||
color: theme?.colorTheme?.black?.withOpacity(0.5) ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
@@ -65,17 +67,27 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
||||
builder: (_) {
|
||||
if (snapshot.hasError) {
|
||||
return widget.errorBuilder?.call(context, snapshot.error) ??
|
||||
Center(child: StreamSvgIcon.error());
|
||||
Center(
|
||||
child: StreamSvgIcon.error(),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return widget.placeholderBuilder?.call(context) ??
|
||||
Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
fit: widget.fit,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
);
|
||||
return Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
child: widget.placeholderBuilder?.call(context) ??
|
||||
Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Image.memory(
|
||||
snapshot.data,
|
||||
|
||||
Reference in New Issue
Block a user