Merge branch 'develop' into feat/messageInput

This commit is contained in:
Sahil Kumar
2023-04-07 03:27:37 +05:30
committed by GitHub
303 changed files with 3808 additions and 1842 deletions
@@ -1,5 +1,4 @@
export 'attachment_error.dart';
export 'attachment_error.dart';
export 'attachment_upload_state_builder.dart';
export 'attachment_widget.dart' show AttachmentSource;
export 'file_attachment.dart';
@@ -195,7 +195,7 @@ class _FileTypeImage extends StatelessWidget {
shape: _getDefaultShape(context),
child: source.when(
local: () => StreamVideoThumbnailImage(
video: attachment.file!.path!,
video: attachment.file!.path,
placeholderBuilder: (_) => const Center(
child: SizedBox(
width: 20,
@@ -205,7 +205,7 @@ class _FileTypeImage extends StatelessWidget {
),
),
network: () => StreamVideoThumbnailImage(
video: attachment.assetUrl!,
video: attachment.assetUrl,
placeholderBuilder: (_) => const Center(
child: SizedBox(
width: 20,
@@ -17,6 +17,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
this.onShowMessage,
this.onReplyMessage,
this.onAttachmentTap,
this.attachmentActionsModalBuilder,
});
/// {@macro showMessageCallback}
@@ -28,6 +29,9 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
final imageUrl =
@@ -260,6 +264,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
);
},
@@ -123,7 +123,9 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
final tempDir = await getTemporaryDirectory();
final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows);
final tempFilePath = tempPath.resolve(fileName!).path;
final tempFilePath = tempPath
.resolve(fileName!)
.toFilePath(windows: CurrentPlatform.isWindows);
print(tempFilePath);
final attachmentFileBytes = attachmentFile.bytes;
@@ -22,6 +22,7 @@ class StreamImageAttachment extends StreamAttachmentWidget {
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
this.attachmentActionsModalBuilder,
});
/// The [StreamMessageThemeData] to use for the image title
@@ -52,26 +53,40 @@ class StreamImageAttachment extends StreamAttachmentWidget {
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.localUri == null || attachment.file?.bytes == null) {
return AttachmentError(constraints: constraints);
}
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
if (attachment.file?.bytes != null) {
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: _imageErrorBuilder,
),
),
);
);
} else if (attachment.localUri != null) {
return _buildImageAttachment(
context,
Image.asset(
attachment.localUri!.path,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: _imageErrorBuilder,
),
);
} else {
return AttachmentError(
constraints: constraints,
);
}
},
network: () {
var imageUrl =
@@ -116,6 +131,12 @@ class StreamImageAttachment extends StreamAttachmentWidget {
);
}
Widget _imageErrorBuilder(BuildContext _, Object __, StackTrace? ___) =>
Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
return Container(
constraints: constraints,
@@ -144,6 +165,8 @@ class StreamImageAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
);
},
@@ -18,6 +18,7 @@ class StreamImageGroup extends StatelessWidget {
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
this.attachmentActionsModalBuilder,
});
/// List of attachments to show
@@ -54,6 +55,9 @@ class StreamImageGroup extends StatelessWidget {
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@@ -154,6 +158,7 @@ class StreamImageGroup extends StatelessWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
),
),
@@ -170,6 +175,7 @@ class StreamImageGroup extends StatelessWidget {
imageThumbnailSize: imageThumbnailSize,
imageThumbnailResizeType: imageThumbnailResizeType,
imageThumbnailCropType: imageThumbnailCropType,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
);
}
}
@@ -107,7 +107,7 @@ class StreamUrlAttachment extends StatelessWidget {
if (urlAttachment.title != null)
Text(
urlAttachment.title!.trim(),
maxLines: 1,
maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w700),
@@ -17,6 +17,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
this.onShowMessage,
this.onReplyMessage,
this.onAttachmentTap,
this.attachmentActionsModalBuilder,
});
/// The [StreamMessageThemeData] to use for the title
@@ -31,6 +32,9 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
return source.when(
@@ -41,7 +45,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.file!.path!,
video: attachment.file!.path,
thumbUrl: attachment.thumbUrl,
constraints: constraints,
),
@@ -54,7 +58,7 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.assetUrl!,
video: attachment.assetUrl,
thumbUrl: attachment.thumbUrl,
constraints: constraints,
),
@@ -86,6 +90,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
),
),
@@ -33,10 +33,10 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to download [attachment].
final AttachmentDownloader? attachmentDownloader;
/// Show reply option
/// Show "reply" option
final bool showReply;
/// Show show in chat option
/// Show "show in chat" option
final bool showShowInChat;
/// Show save option
@@ -55,6 +55,7 @@ class AttachmentActionsModal extends StatelessWidget {
Attachment? attachment,
Message? message,
VoidCallback? onShowMessage,
VoidCallback? onReply,
AttachmentDownloader? attachmentDownloader,
bool? showReply,
bool? showShowInChat,
@@ -67,6 +68,7 @@ class AttachmentActionsModal extends StatelessWidget {
attachment: attachment ?? this.attachment,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
onReply: onReply ?? this.onReply,
attachmentDownloader: attachmentDownloader ?? this.attachmentDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
@@ -112,7 +114,7 @@ class AttachmentActionsModal extends StatelessWidget {
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() => Navigator.of(context).pop(ReturnActionType.reply),
onReply,
),
if (showShowInChat)
_buildButton(
@@ -162,8 +162,8 @@ class StreamChannelHeader extends StatelessWidget
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -131,8 +131,8 @@ class StreamChannelListHeader extends StatelessWidget
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -181,7 +181,10 @@ class StreamChannelListHeader extends StatelessWidget
package: 'stream_chat_flutter',
width: 24,
height: 24,
color: color,
colorFilter: ColorFilter.mode(
color,
BlendMode.srcIn,
),
);
},
),
@@ -1,4 +1,8 @@
// TODO: remove in v6 as this is no longer used. Currently exported.
/// Return action for coming back from pages
@Deprecated('''
ReturnActionType has been deprecated and is no longer used.''')
enum ReturnActionType {
/// No return action
none,
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
@@ -61,6 +60,7 @@ class StreamGalleryFooter extends StatefulWidget
class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
final shareButtonKey = GlobalKey();
@override
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
@@ -108,12 +108,19 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
await file.writeAsBytes(bytes);
final box =
shareButtonKey.currentContext?.findRenderObject();
final size = shareButtonKey.currentContext?.size;
final position =
(box! as RenderBox).localToGlobal(Offset.zero);
await Share.shareXFiles(
[XFile(filePath)],
sharePositionOrigin:
Rect.fromLTWH(position.dx, position.dy, 0, 0),
sharePositionOrigin: Rect.fromLTWH(
position.dx,
position.dy,
size?.width ?? 50,
(size?.height ?? 2) / 2,
),
);
},
),
@@ -219,8 +226,8 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
child: AspectRatio(
aspectRatio: 1,
child: StreamVideoThumbnailImage(
video: (attachment.file?.path ??
attachment.assetUrl)!,
video:
attachment.file?.path ?? attachment.assetUrl,
),
),
),
@@ -80,8 +80,8 @@ class StreamGalleryHeader extends StatelessWidget
final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context);
final theme = Theme.of(context);
return AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -18,6 +18,9 @@ abstract class Translations {
/// The label for showing no users
String get noUsersLabel;
/// The label for showing no photo or video
String get noPhotoOrVideoLabel;
/// The text for showing user is online
String get userOnlineText;
@@ -78,7 +81,10 @@ abstract class Translations {
/// The text for showing the unread messages count
/// in the [StreamMessageListView]
String unreadMessagesSeparatorText(int unreadCount);
String unreadMessagesSeparatorText(
@Deprecated('unreadCount is not used anymore and will be removed ')
int unreadCount,
);
/// The label for "connected" in [StreamConnectionStatusBuilder]
String get connectedLabel;
@@ -373,6 +379,9 @@ class DefaultTranslations implements Translations {
@override
String get noUsersLabel => 'There are no users currently';
@override
String get noPhotoOrVideoLabel => 'There is no photo or video';
@override
String get retryLabel => 'Retry';
@@ -791,12 +800,7 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments
String get linkDisabledError => 'Links are disabled';
@override
String unreadMessagesSeparatorText(int unreadCount) {
if (unreadCount == 1) {
return '1 unread message';
}
return '$unreadCount unread messages';
}
String unreadMessagesSeparatorText(int unreadCount) => 'New messages';
@override
String get enableFileAccessMessage => 'Please enable access to files'
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:stream_chat_flutter/src/message_actions_modal/mam_widgets.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template messageActionsModal}
@@ -97,34 +98,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Widget _showMessageOptionsModal() {
final mediaQueryData = MediaQuery.of(context);
final size = mediaQueryData.size;
final user = StreamChat.of(context).currentUser;
final orientation = mediaQueryData.orientation;
final roughMaxSize = size.width * 2 / 3;
var messageTextLength = widget.message.text!.length;
if (widget.message.quotedMessage != null) {
var quotedMessageLength =
(widget.message.quotedMessage!.text?.length ?? 0) + 40;
if (widget.message.quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize = messageTextLength *
(widget.messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2;
final divFactor = widget.message.attachments.isNotEmpty
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final fontSize = widget.messageTheme.messageTextStyle?.fontSize;
final streamChatThemeData = StreamChatTheme.of(context);
final numberOfReactions =
StreamChatConfiguration.of(context).reactionIcons.length;
final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
final channel = StreamChannel.of(context).channel;
final child = Center(
@@ -142,11 +121,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
builder: (context, constraints) {
return Align(
alignment: Alignment(
_calculateReactionsHorizontalAlignmentValue(
calculateReactionsHorizontalAlignment(
user,
divFactor,
shiftFactor,
widget.message,
constraints,
fontSize,
orientation,
),
0,
),
@@ -156,7 +136,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
);
},
),
const SizedBox(height: 8),
const SizedBox(height: 10),
IgnorePointer(
child: widget.messageWidget,
),
@@ -281,93 +261,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
);
}
double _calculateReactionsHorizontalAlignmentValue(
User? user,
num divFactor,
double shiftFactor,
BoxConstraints constraints,
) {
var result = 0.0;
var cont = true;
if (user?.id == widget.message.user?.id) {
if (divFactor >= 1.0) {
// This calculation is hacky and does not cover all bases!!!
// A better option is needed!
// Landscape calculations
if (constraints.maxWidth == 1350) {
// 12.7 iPad Pro
result = shiftFactor + 0.5;
cont = false;
} else if (constraints.maxWidth == 1178) {
// 11 inch iPad Pro
result = shiftFactor + 0.42;
cont = false;
} else if (constraints.maxWidth == 1164) {
// iPad Air 4
result = shiftFactor + 0.4;
cont = false;
} else if (constraints.maxWidth == 1117) {
// iPad Mini 6
result = shiftFactor + 0.37;
cont = false;
} else if (constraints.maxWidth == 1064) {
// iPad 9th gen
result = shiftFactor + 0.33;
cont = false;
} else if (constraints.maxWidth == 1008) {
// 9.7 inch iPad Pro
result = shiftFactor + 0.3;
cont = false;
} else if (constraints.maxWidth >= 200 && constraints.maxWidth <= 400) {
// Phone (?)
result = shiftFactor - 0.2;
cont = false;
}
if (cont) {
// Portrait calculations
if (constraints.maxWidth == 1008) {
// 12.7 iPad Pro
result = shiftFactor + 0.3;
} else if (constraints.maxWidth == 818) {
// 11 inch iPad Pro
result = shiftFactor + 0.07;
} else if (constraints.maxWidth == 804) {
// iPad Air 4
result = shiftFactor + 0.04;
} else if (constraints.maxWidth == 794) {
// iPad 9th gen
result = shiftFactor + 0.02;
} else if (constraints.maxWidth >= 752) {
// 9.7 inch iPad Pro
result = shiftFactor - 0.05;
} else if (constraints.maxWidth == 728) {
// iPad Mini 6
result = shiftFactor - 0.1;
}
}
} else {
result = 1.2 - divFactor;
}
} else {
if (divFactor >= 1.0) {
result = shiftFactor + 0.2;
} else {
result = -(1.2 - divFactor);
}
}
// Ensure reactions don't get pushed past the edge of the screen.
//
// Hacky!!! Needs improvement!!!
if (result > 1) {
return 1;
} else {
return result;
}
}
InkWell _buildCustomAction(
BuildContext context,
StreamMessageAction messageAction,
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Shows a modal material design bottom sheet.
@@ -107,44 +107,34 @@ Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
controller: controller,
initialAttachments: initialAttachments,
builder: (context, controller, child) {
return PlatformWidget(
web: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
mobile: (context) {
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
desktop: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
final currentPlatform = defaultTargetPlatform;
final isWebOrDesktop = kIsWeb ||
currentPlatform == TargetPlatform.macOS ||
currentPlatform == TargetPlatform.linux ||
currentPlatform == TargetPlatform.windows;
if (isWebOrDesktop) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
}
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
);
@@ -276,7 +276,7 @@ class _ParseAttachments extends StatelessWidget {
'video': (_, attachment) {
return StreamVideoThumbnailImage(
key: ValueKey(attachment.assetUrl),
video: attachment.file?.path ?? attachment.assetUrl!,
video: attachment.file?.path ?? attachment.assetUrl,
constraints: BoxConstraints.loose(const Size(32, 32)),
errorBuilder: (_, __) => AttachmentError(
constraints: BoxConstraints.loose(const Size(32, 32)),
@@ -1220,7 +1220,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
104,
),
),
video: (attachment.file?.path ?? attachment.assetUrl)!,
video: attachment.file?.path ?? attachment.assetUrl,
),
Positioned(
left: 8,
@@ -80,7 +80,6 @@ class StreamMessageTextField extends StatefulWidget {
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '',
@@ -155,31 +154,7 @@ class StreamMessageTextField extends StatefulWidget {
keyboardType = keyboardType ??
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection =
enableInteractiveSelection ?? (!readOnly || !obscureText),
toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true,
paste: true,
))
: (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
)));
enableInteractiveSelection ?? (!readOnly || !obscureText);
/// Controls the message being edited.
///
@@ -304,13 +279,6 @@ class StreamMessageTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.readOnly}
final bool readOnly;
/// Configuration of toolbar options.
///
/// If not set, select all and paste will default to be enabled. Copy and cut
/// will be disabled if [obscureText] is true. If [readOnly] is true,
/// paste and cut will be disabled regardless.
final ToolbarOptions toolbarOptions;
/// {@macro flutter.widgets.editableText.showCursor}
final bool? showCursor;
@@ -720,7 +688,6 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
textAlignVertical: widget.textAlignVertical,
textDirection: widget.textDirection,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
@@ -24,9 +24,7 @@ class UnreadMessagesSeparator extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
context.translations.unreadMessagesSeparatorText(
unreadCount,
),
context.translations.unreadMessagesSeparatorText(unreadCount),
textAlign: TextAlign.center,
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
),
@@ -32,6 +32,7 @@ class BottomRow extends StatelessWidget {
this.deletedBottomRowBuilder,
this.onThreadTap,
this.usernameBuilder,
this.sendingIndicatorBuilder,
});
/// {@macro messageIsDeleted}
@@ -88,6 +89,61 @@ class BottomRow extends StatelessWidget {
/// {@macro usernameBuilder}
final Widget Function(BuildContext, Message)? usernameBuilder;
/// {@macro sendingIndicatorBuilder}
final Widget Function(BuildContext, Message)? sendingIndicatorBuilder;
/// {@template copyWith}
/// Creates a copy of [BottomRow] with specified attributes
/// overridden.
/// {@endtemplate}
BottomRow copyWith({
Key? key,
bool? isDeleted,
Message? message,
bool? showThreadReplyIndicator,
bool? showInChannel,
bool? showTimeStamp,
bool? showUsername,
bool? reverse,
bool? showSendingIndicator,
bool? hasUrlAttachments,
bool? isGiphy,
bool? isOnlyEmoji,
StreamMessageThemeData? messageTheme,
StreamChatThemeData? streamChatTheme,
bool? hasNonUrlAttachments,
StreamChatState? streamChat,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(Message)? onThreadTap,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? sendingIndicatorBuilder,
}) =>
BottomRow(
key: key ?? this.key,
isDeleted: isDeleted ?? this.isDeleted,
message: message ?? this.message,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannel: showInChannel ?? this.showInChannel,
showTimeStamp: showTimeStamp ?? this.showTimeStamp,
showUsername: showUsername ?? this.showUsername,
reverse: reverse ?? this.reverse,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
hasUrlAttachments: hasUrlAttachments ?? this.hasUrlAttachments,
isGiphy: isGiphy ?? this.isGiphy,
isOnlyEmoji: isOnlyEmoji ?? this.isOnlyEmoji,
messageTheme: messageTheme ?? this.messageTheme,
streamChatTheme: streamChatTheme ?? this.streamChatTheme,
hasNonUrlAttachments: hasNonUrlAttachments ?? this.hasNonUrlAttachments,
streamChat: streamChat ?? this.streamChat,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
sendingIndicatorBuilder:
sendingIndicatorBuilder ?? this.sendingIndicatorBuilder,
);
@override
Widget build(BuildContext context) {
if (isDeleted) {
@@ -147,13 +203,14 @@ class BottomRow extends StatelessWidget {
),
if (showSendingIndicator)
WidgetSpan(
child: SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
child: sendingIndicatorBuilder?.call(context, message) ??
SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
),
]);
@@ -40,7 +40,8 @@ class StreamMessageText extends StatelessWidget {
.translate(language)
.replaceMentions()
.text
?.replaceAll('\n', '\n\n');
?.replaceAll('\n', '\n\n')
.trim();
final themeData = Theme.of(context);
return MarkdownBody(
data: messageText ?? '',
@@ -8,6 +8,7 @@ import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction
import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart';
import 'package:stream_chat_flutter/src/dialogs/dialogs.dart';
import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart';
import 'package:stream_chat_flutter/src/message_widget/bottom_row.dart';
import 'package:stream_chat_flutter/src/message_widget/message_widget_content.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/message_reactions_modal.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -79,8 +80,15 @@ class StreamMessageWidget extends StatefulWidget {
this.userAvatarBuilder,
this.editMessageInputBuilder,
this.textBuilder,
this.bottomRowBuilder,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.customAttachmentBuilders,
this.padding,
this.textPadding = const EdgeInsets.symmetric(
@@ -92,11 +100,19 @@ class StreamMessageWidget extends StatefulWidget {
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
this.usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
this.imageAttachmentThumbnailSize = const Size(400, 400),
this.imageAttachmentThumbnailResizeType = 'clip',
this.imageAttachmentThumbnailCropType = 'center',
}) : attachmentBuilders = {
this.attachmentActionsModalBuilder,
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
),
attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
side: attachmentBorderSide ??
@@ -129,6 +145,8 @@ class StreamMessageWidget extends StatefulWidget {
imageThumbnailResizeType:
imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
),
),
attachmentShape: border,
@@ -156,6 +174,7 @@ class StreamMessageWidget extends StatefulWidget {
imageThumbnailSize: imageAttachmentThumbnailSize,
imageThumbnailResizeType: imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder: attachmentActionsModalBuilder,
),
attachmentShape: border,
);
@@ -189,6 +208,8 @@ class StreamMessageWidget extends StatefulWidget {
onAttachmentTap(message, attachment);
}
: null,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder,
);
}).toList(),
),
@@ -306,7 +327,13 @@ class StreamMessageWidget extends StatefulWidget {
/// {@template bottomRowBuilder}
/// Widget builder for building a bottom row below the message
/// {@endtemplate}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@template bottomRowBuilderWithDefaultWidget}
/// Widget builder for building a bottom row below the message.
/// Also contains the default bottom row widget.
/// {@endtemplate}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@template deletedBottomRowBuilder}
/// Widget builder for building a bottom row below a deleted message
@@ -512,6 +539,9 @@ class StreamMessageWidget extends StatefulWidget {
/// {@macro onMessageWidgetAttachmentTap}
final OnMessageWidgetAttachmentTap? onAttachmentTap;
/// {@macro attachmentActionsBuilder}
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
/// Size of the image attachment thumbnail.
final Size imageAttachmentThumbnailSize;
@@ -537,9 +567,19 @@ class StreamMessageWidget extends StatefulWidget {
void Function(Message)? onReplyTap,
Widget Function(BuildContext, Message)? editMessageInputBuilder,
Widget Function(BuildContext, Message)? textBuilder,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? bottomRowBuilder,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') BottomRowBuilder? bottomRowBuilder,
BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
StreamMessageThemeData? messageTheme,
@@ -586,7 +626,31 @@ class StreamMessageWidget extends StatefulWidget {
Size? imageAttachmentThumbnailSize,
String? imageAttachmentThumbnailResizeType,
String? imageAttachmentThumbnailCropType,
AttachmentActionsBuilder? attachmentActionsModalBuilder,
}) {
assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
var _bottomRowBuilderWithDefaultWidget =
bottomRowBuilderWithDefaultWidget ??
this.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??= (context, message, defaultWidget) {
final _bottomRowBuilder = bottomRowBuilder ?? this.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
);
};
return StreamMessageWidget(
key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap,
@@ -595,10 +659,7 @@ class StreamMessageWidget extends StatefulWidget {
editMessageInputBuilder:
editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget,
onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme,
@@ -652,6 +713,8 @@ class StreamMessageWidget extends StatefulWidget {
this.imageAttachmentThumbnailResizeType,
imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ??
this.imageAttachmentThumbnailCropType,
attachmentActionsModalBuilder:
attachmentActionsModalBuilder ?? this.attachmentActionsModalBuilder,
);
}
@@ -838,52 +901,69 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
? Alignment.centerRight
: Alignment.centerLeft,
widthFactor: widget.widthFactor,
child: MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
onThreadTap: widget.onThreadTap,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilder: widget.bottomRowBuilder,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
usernameBuilder: widget.usernameBuilder,
),
child: Builder(builder: (context) {
var _bottomRowBuilderWithDefaultWidget =
widget.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??=
(context, message, defaultWidget) {
final _bottomRowBuilder = widget.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: widget.onThreadTap,
usernameBuilder: widget.usernameBuilder,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
);
};
return MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilderWithDefaultWidget:
_bottomRowBuilderWithDefaultWidget,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
);
}),
),
),
),
@@ -4,6 +4,18 @@ import 'package:stream_chat_flutter/src/message_widget/message_widget_content_co
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message].
typedef BottomRowBuilder = Widget Function(BuildContext, Message);
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message] and the default [BottomRow].
typedef BottomRowBuilderWithDefaultWidget = Widget Function(
BuildContext,
Message,
BottomRow,
);
/// {@template messageWidgetContent}
/// The main content of a [StreamMessageWidget].
///
@@ -51,12 +63,28 @@ class MessageWidgetContent extends StatelessWidget {
this.onMentionTap,
this.onLinkTap,
this.textBuilder,
this.bottomRowBuilder,
this.onThreadTap,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.onThreadTap,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.userAvatarBuilder,
this.usernameBuilder,
});
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
/// {@macro reverse}
final bool reverse;
@@ -152,7 +180,10 @@ class MessageWidgetContent extends StatelessWidget {
final double bottomRowPadding;
/// {@macro bottomRowBuilder}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@macro bottomRowBuilderWithDefaultWidget}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@macro showInChannelIndicator}
final bool showInChannel;
@@ -207,30 +238,7 @@ class MessageWidgetContent extends StatelessWidget {
right: reverse ? bottomRowPadding : 0,
bottom: isPinned && showPinHighlight ? 6.0 : 0.0,
),
child: bottomRowBuilder?.call(
context,
message,
) ??
BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
),
child: _buildBottomRow(context),
),
Padding(
padding: EdgeInsets.only(
@@ -457,4 +465,39 @@ class MessageWidgetContent extends StatelessWidget {
),
);
}
Widget _buildBottomRow(BuildContext context) {
final defaultWidget = BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
);
if (bottomRowBuilder != null) {
return bottomRowBuilder!(context, message);
} else if (bottomRowBuilderWithDefaultWidget != null) {
return bottomRowBuilderWithDefaultWidget!(
context,
message,
defaultWidget,
);
}
return defaultWidget;
}
}
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template streamMessageReactionsModal}
@@ -39,35 +40,13 @@ class StreamMessageReactionsModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).currentUser;
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
final orientation = MediaQuery.of(context).orientation;
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final roughMaxSize = size.width * 2 / 3;
var messageTextLength = message.text!.length;
if (message.quotedMessage != null) {
var quotedMessageLength = message.quotedMessage!.text!.length + 40;
if (message.quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize = messageTextLength *
(messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2;
final divFactor = message.attachments.isNotEmpty
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final numberOfReactions =
StreamChatConfiguration.of(context).reactionIcons.length;
final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
final fontSize = messageTheme.messageTextStyle?.fontSize;
final child = Center(
child: SingleChildScrollView(
@@ -79,22 +58,26 @@ class StreamMessageReactionsModal extends StatelessWidget {
children: <Widget>[
if ((showReactions ?? hasReactionPermission) &&
(message.status == MessageSendingStatus.sent))
Align(
alignment: Alignment(
user!.id == message.user!.id
? (divFactor >= 1.0
? -0.2 - shiftFactor
: (1.2 - divFactor))
: (divFactor >= 1.0
? shiftFactor + 0.2
: -(1.2 - divFactor)),
0,
),
child: StreamReactionPicker(
message: message,
),
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
message,
constraints,
fontSize,
orientation,
),
0,
),
child: StreamReactionPicker(
message: message,
),
);
},
),
const SizedBox(height: 8),
const SizedBox(height: 10),
IgnorePointer(
child: messageWidget,
),
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// This method calculates the align that the modal of reactions should have.
/// This is an approximation based on the size of the message and the
/// available space in the screen.
double calculateReactionsHorizontalAlignment(
User? user,
Message message,
BoxConstraints constraints,
double? fontSize,
Orientation orientation,
) {
final maxWidth = constraints.maxWidth;
final roughSentenceSize = message.roughMessageSize(fontSize);
final hasAttachments = message.attachments.isNotEmpty;
final isReply = message.quotedMessageId != null;
final isAttachment = hasAttachments && !isReply;
// divFactor is the percentage of the available space that the message takes.
// When the divFactor is bigger than 0.5 that means that the messages is
// bigger than 50% of the available space and the modal should have an offset
// in the direction that the message grows. When the divFactor is smaller
// than 0.5 then the offset should be to he side opposite of the message
// growth.
// In resume, when divFactor > 0.5 then result > 0, when divFactor < 0.5
// then result < 0.
var divFactor = 0.5;
// When in portrait, attachments normally take 75% of the screen, when in
// landscape, attachments normally take 50% of the screen.
if (isAttachment) {
if (orientation == Orientation.portrait) {
divFactor = 0.75;
} else {
divFactor = 0.5;
}
} else {
divFactor = roughSentenceSize == 0 ? 0.5 : (roughSentenceSize / maxWidth);
}
final signal = user?.id == message.user?.id ? 1 : -1;
final result = signal * (1 - divFactor * 2.0);
return _capResult(result);
}
// Ensure reactions don't get pushed past the edge of the screen.
//
// This happens if divFactor is really big. When this happens, we can simply
// move the model all the way to the end of screen.
double _capResult(double result) {
if (result > 1.0) {
return 1;
} else if (result < -1.0) {
return -1;
} else {
return result;
}
}
@@ -1088,7 +1088,12 @@ class StreamSvgIcon extends StatelessWidget {
key: key,
width: width,
height: height,
color: color,
colorFilter: color != null
? ColorFilter.mode(
color!,
BlendMode.srcIn,
)
: null,
);
}
}
@@ -149,8 +149,8 @@ class StreamThreadHeader extends StatelessWidget
final theme = Theme.of(context);
return AppBar(
automaticallyImplyLeading: false,
toolbarTextStyle: theme.textTheme.bodyText2,
titleTextStyle: theme.textTheme.headline6,
toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
@@ -409,7 +409,7 @@ class StreamChannelListErrorWidget extends StatelessWidget {
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: onPressed,
@@ -122,7 +122,8 @@ class StreamMessageSearchListTile extends StatelessWidget {
final title = this.title ??
MessageSearchListTileTitle(
messageResponse: messageResponse,
textStyle: channelPreviewTheme.titleStyle,
textStyle: channelPreviewTheme.titleStyle
?.copyWith(overflow: TextOverflow.ellipsis),
);
final subtitle = this.subtitle ??
@@ -177,27 +178,28 @@ class MessageSearchListTileTitle extends StatelessWidget {
final channel = messageResponse.channel;
final channelName = channel?.extraData['name'];
return Row(
children: [
Text(
user.id == StreamChat.of(context).currentUser?.id
? context.translations.youText
: user.name,
style: textStyle,
),
if (channelName != null) ...[
Text(
' ${context.translations.inText} ',
style: textStyle?.copyWith(
fontWeight: FontWeight.normal,
return Text.rich(
TextSpan(
children: [
TextSpan(
text: user.id == StreamChat.of(context).currentUser?.id
? context.translations.youText
: user.name,
),
if (channelName != null) ...[
TextSpan(
text: ' ${context.translations.inText} ',
style: textStyle?.copyWith(
fontWeight: FontWeight.normal,
),
),
),
Text(
channelName as String,
style: textStyle,
),
TextSpan(
text: channelName as String,
),
],
],
],
),
style: textStyle,
);
}
}
@@ -366,7 +366,7 @@ class StreamPhotoGallery extends StatelessWidget {
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
context.translations.noPhotoOrVideoLabel,
style: chatThemeData.textTheme.headline,
),
),
@@ -377,7 +377,7 @@ class StreamPhotoGallery extends StatelessWidget {
return StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
context.translations.genericErrorText,
textAlign: TextAlign.center,
),
);
@@ -400,7 +400,7 @@ class StreamPhotoGallery extends StatelessWidget {
return errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
errorTitle: Text(context.translations.genericErrorText),
onRetryPressed: controller.refresh,
),
);
@@ -21,6 +21,7 @@ class StreamMessageThemeData with Diagnosticable {
this.avatarTheme,
this.createdAtStyle,
this.linkBackgroundColor,
this.urlAttachmentTitleMaxLine,
});
/// Text style for message text
@@ -59,6 +60,9 @@ class StreamMessageThemeData with Diagnosticable {
/// Background color for messages with url attachments.
final Color? linkBackgroundColor;
/// Max number of lines in Url link title
final int? urlAttachmentTitleMaxLine;
/// Copy with a theme
StreamMessageThemeData copyWith({
TextStyle? messageTextStyle,
@@ -73,6 +77,7 @@ class StreamMessageThemeData with Diagnosticable {
Color? reactionsBorderColor,
Color? reactionsMaskColor,
Color? linkBackgroundColor,
int? urlAttachmentTitleMaxLine,
}) {
return StreamMessageThemeData(
messageTextStyle: messageTextStyle ?? this.messageTextStyle,
@@ -89,6 +94,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
urlAttachmentTitleMaxLine:
urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine,
);
}
@@ -148,6 +155,7 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor: other.reactionsBorderColor,
reactionsMaskColor: other.reactionsMaskColor,
linkBackgroundColor: other.linkBackgroundColor,
urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine,
);
}
@@ -167,7 +175,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor == other.reactionsBorderColor &&
reactionsMaskColor == other.reactionsMaskColor &&
avatarTheme == other.avatarTheme &&
linkBackgroundColor == other.linkBackgroundColor;
linkBackgroundColor == other.linkBackgroundColor &&
urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine;
@override
int get hashCode =>
@@ -182,7 +191,8 @@ class StreamMessageThemeData with Diagnosticable {
reactionsBorderColor.hashCode ^
reactionsMaskColor.hashCode ^
avatarTheme.hashCode ^
linkBackgroundColor.hashCode;
linkBackgroundColor.hashCode ^
urlAttachmentTitleMaxLine.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -199,6 +209,10 @@ class StreamMessageThemeData with Diagnosticable {
..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor))
..add(ColorProperty('reactionsBorderColor', reactionsBorderColor))
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor))
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor));
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor))
..add(DiagnosticsProperty(
'urlAttachmentTitleMaxLine',
urlAttachmentTitleMaxLine,
));
}
}
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
@@ -12,6 +14,16 @@ extension StringExtension on String {
String capitalize() =>
isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
/// Returns the biggest line of a text.
String biggestLine() {
if (contains('\n')) {
return split('\n')
.reduce((curr, next) => curr.length > next.length ? curr : next);
} else {
return this;
}
}
/// Returns whether the string contains only emoji's or not.
///
/// Emojis guidelines
@@ -353,6 +365,33 @@ extension MessageX on Message {
return copyWith(text: messageTextToRender);
}
/// Returns an approximation of message size
double roughMessageSize(double? fontSize) {
var messageTextLength = min(text!.biggestLine().length, 65);
if (quotedMessage != null) {
var quotedMessageLength =
(min(quotedMessage!.text?.biggestLine().length ?? 0, 65)) + 8;
if (quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 8;
}
if (quotedMessageLength > messageTextLength * 1.2) {
messageTextLength = quotedMessageLength;
}
}
// Quoted message have a smaller font, so it is necessary to reduce the
// size of the multiplier to count for the smaller font.
var multiplier = 0.55;
if (quotedMessage != null) {
multiplier = 0.45;
}
return messageTextLength * (fontSize ?? 1) * multiplier;
}
/// It returns the message with the translated text if available locally
Message translate(String language) =>
copyWith(text: i18n?['${language}_text'] ?? text);
@@ -21,6 +21,9 @@ class _IVideoService {
///
/// Thumbnails are not supported on Web at this time.
///
/// If no [video] path is supplied, or if a thumbnail cannot be generated,
/// returns [generatePlaceholderThumbnail]. A stock placeholder image.
///
/// For desktop, you can specify the position of the video to generate
/// the thumbnail.
///
@@ -29,14 +32,14 @@ class _IVideoService {
/// creates lower quality of the thumbnail image, but it gets ignored for
/// PNG format.
Future<Uint8List?> generateVideoThumbnail({
required String video,
String? video,
ImageFormat imageFormat = ImageFormat.PNG,
int maxHeight = 0,
int maxWidth = 0,
int timeMs = 0,
int quality = 10,
}) async {
if (kIsWeb) {
if (kIsWeb || video == null) {
final placeholder = await generatePlaceholderThumbnail();
return placeholder;
}
@@ -8,13 +8,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
/// {@template streamVideoThumbnailImage}
/// Displays a video thumbnail for video attachments in a message.
/// Displays a video thumbnail for video attachments.
///
/// [thumbUrl] is used if provided.
///
/// Else [video] (path to local or remote video) is used to generate
/// a thumbnail from the video asset.
///
/// WARNING! a local path does not work on web.
///
/// If both [thumbUrl] and [video] are null, or if a thumbnail can't be
/// generated, a stock default image will be used.
/// {@endtemplate}
class StreamVideoThumbnailImage extends StatefulWidget {
/// {@macro streamVideoThumbnailImage}
const StreamVideoThumbnailImage({
super.key,
required this.video,
this.video,
this.thumbUrl,
this.constraints,
this.fit = BoxFit.cover,
@@ -24,7 +34,7 @@ class StreamVideoThumbnailImage extends StatefulWidget {
});
/// Video path or url
final String video;
final String? video;
/// Video thumbnail url
final String? thumbUrl;
@@ -1,6 +1,6 @@
import 'package:stream_chat_flutter/src/video/vlc/vlc_stub.dart'
if (dart.library.io) 'vlc_manager_desktop.dart'
if (dar.library.html) 'vlc_manager_web.dart';
if (dart.library.html) 'vlc_manager_web.dart';
/// {@template vlcManager}
/// An abstract class for the purpose of ensuring Flutter applications that