chore: merge latest dev

This commit is contained in:
groovinchip
2021-08-02 13:15:28 -04:00
176 changed files with 7089 additions and 489 deletions
+13 -1
View File
@@ -1,9 +1,21 @@
## Upcoming
## 2.1.2
🐞 Fixed
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending message
## 2.1.1
- Updated core dependency
## 2.1.0
✅ Added
- Added `MessageListView.paginationLimit`
- `MessageText` renders message translation if available
- Allow the various ListView widgets to be themed via ThemeData classes
- Added `bottomRowBuilder` and `deletedBottomRowBuilder` that build a widget below a `MessageWidget`
🔄 Changed
+2 -1
View File
@@ -14,7 +14,8 @@
- [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat
- [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/)
- [Chat UI Kit](https://getstream.io/chat/ui-kit/)
- [UI Docs](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/introduction/)
- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart)
### Changelog
@@ -1,11 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// A chat-persisted StreamChatClient
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
);
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -15,7 +12,7 @@ void main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
);
/// Set the current user and connect the websocket. In a production
/// scenario, this should be done using a backend to generate a user token
@@ -72,6 +69,13 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
client: client,
child: widget,
@@ -33,6 +33,8 @@ dependencies:
# path: ../../stream_chat_flutter_core
stream_chat_flutter:
path: ../
stream_chat_localizations:
path: ../../stream_chat_localizations
stream_chat_persistence:
path: ../../stream_chat_persistence
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget to build in progress
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
@@ -226,7 +227,7 @@ class _FailedState extends StatelessWidget {
horizontal: 12,
),
child: Text(
'UPLOAD ERROR',
context.translations.uploadErrorLabel,
style: theme.textTheme.footnote.copyWith(
color: theme.colorTheme.barsBg,
),
@@ -7,9 +7,9 @@ import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
// ignore: always_use_package_imports
import 'attachment_widget.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
/// Widget for displaying file attachments
class FileAttachment extends AttachmentWidget {
@@ -76,7 +76,7 @@ class FileAttachment extends AttachmentWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attachment.title ?? 'File',
attachment.title ?? context.translations.fileText,
style: StreamChatTheme.of(context).textTheme.bodyBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -286,7 +286,10 @@ class FileAttachment extends AttachmentWidget {
progressIndicatorColor: theme.colorTheme.accentPrimary,
),
success: () => Text(fileSize(size), style: textStyle),
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
failed: (_) => Text(
context.translations.uploadErrorLabel,
style: textStyle,
),
);
}
}
@@ -2,8 +2,10 @@ 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_widget.dart';
import 'package:stream_chat_flutter/src/visible_footnote.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget {
@@ -71,9 +73,9 @@ class GiphyAttachment extends AttachmentWidget {
children: [
StreamSvgIcon.giphyIcon(),
const SizedBox(width: 8),
const Text(
'Giphy',
style: TextStyle(fontWeight: FontWeight.bold),
Text(
context.translations.giphyLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
if (attachment.title != null)
@@ -134,7 +136,7 @@ class GiphyAttachment extends AttachmentWidget {
});
},
child: Text(
'Cancel',
context.translations.cancelLabel.toLowerCase(),
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
@@ -166,7 +168,7 @@ class GiphyAttachment extends AttachmentWidget {
});
},
child: Text(
'Shuffle',
context.translations.shuffleLabel,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
@@ -199,7 +201,7 @@ class GiphyAttachment extends AttachmentWidget {
});
},
child: Text(
'Send',
context.translations.sendLabel,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
@@ -216,36 +218,11 @@ class GiphyAttachment extends AttachmentWidget {
),
),
const SizedBox(height: 4),
Align(
const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
size: 16,
),
const SizedBox(
width: 8,
),
Text(
'Only visible to you',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5)),
),
],
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: VisibleFootnote(),
),
),
],
@@ -339,7 +316,7 @@ class GiphyAttachment extends AttachmentWidget {
size: 16,
),
Text(
'GIPHY',
context.translations.giphyLabel.toUpperCase(),
style: TextStyle(
color:
StreamChatTheme.of(context).colorTheme.barsBg,
@@ -48,7 +48,7 @@ class AttachmentActionsModal extends StatelessWidget {
child: _buildPage(context),
);
Widget _buildPage(context) {
Widget _buildPage(BuildContext context) {
final theme = StreamChatTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -69,7 +69,7 @@ class AttachmentActionsModal extends StatelessWidget {
children: [
_buildButton(
context,
'Reply',
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
@@ -80,7 +80,7 @@ class AttachmentActionsModal extends StatelessWidget {
),
_buildButton(
context,
'Show in Chat',
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
@@ -89,8 +89,9 @@ class AttachmentActionsModal extends StatelessWidget {
),
_buildButton(
context,
// ignore: lines_longer_than_80_chars
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
@@ -142,7 +143,7 @@ class AttachmentActionsModal extends StatelessWidget {
message.user?.id)
_buildButton(
context,
'Delete',
context.translations.deleteLabel.capitalize(),
StreamSvgIcon.delete(
size: 24,
color: theme.colorTheme.accentError,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Bottom Sheet with options
class ChannelBottomSheet extends StatefulWidget {
@@ -149,7 +150,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: 'View Info',
title: context.translations.viewInfoLabel,
onTap: widget.onViewInfoTap,
),
if (!channel.isDistinct)
@@ -160,7 +161,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: 'Leave Group',
title: context.translations.leaveGroupLabel,
onTap: () async {
setState(() {
_showActions = false;
@@ -179,7 +180,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.accentError,
),
),
title: 'Delete Conversation',
title: context.translations.deleteConversationLabel,
titleColor: _streamChatThemeData.colorTheme.accentError,
onTap: () async {
setState(() {
@@ -198,7 +199,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: 'Cancel',
title: context.translations.cancelLabel,
onTap: () {
Navigator.pop(context);
},
@@ -219,10 +220,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Future<void> _showDeleteDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
title: context.translations.deleteConversationLabel,
okText: context.translations.deleteLabel,
question: context.translations.deleteConversationQuestion,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete(
color: _streamChatThemeData.colorTheme.accentError,
),
@@ -237,10 +238,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Future<void> _showLeaveDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Leave conversation',
okText: 'LEAVE',
question: 'Are you sure you want to leave this conversation?',
cancelText: 'CANCEL',
title: context.translations.leaveConversationLabel,
okText: context.translations.leaveLabel,
question: context.translations.leaveConversationQuestion,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.userRemove(
color: _streamChatThemeData.colorTheme.accentError,
),
@@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
@@ -121,14 +122,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
statusString = context.translations.connectedLabel;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
statusString = context.translations.reconnectingLabel;
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
statusString = context.translations.disconnectedLabel;
break;
}
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget which shows channel info
class ChannelInfo extends StatelessWidget {
@@ -55,10 +56,13 @@ class ChannelInfo extends StatelessWidget {
) {
Widget? alternativeWidget;
if (channel.memberCount != null && channel.memberCount! > 2) {
var text = '${channel.memberCount} Members';
final memberCount = channel.memberCount;
if (memberCount != null && memberCount > 2) {
var text = context.translations.membersCountText(memberCount);
final watcherCount = channel.state?.watcherCount ?? 0;
if (watcherCount > 0) text += ' $watcherCount Online';
if (watcherCount > 0) {
text += ' ${context.translations.watchersCountText(watcherCount)}';
}
alternativeWidget = Text(
text,
style: StreamChatTheme.of(context).channelHeaderTheme.subtitleStyle,
@@ -72,12 +76,13 @@ class ChannelInfo extends StatelessWidget {
if (otherMember != null) {
if (otherMember.user?.online == true) {
alternativeWidget = Text(
'Online',
context.translations.userOnlineText,
style: textStyle,
);
} else {
alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.user?.lastActive).fromNow()}',
'${context.translations.userLastOnlineText} '
'${Jiffy(otherMember.user?.lastActive).fromNow()}',
style: textStyle,
);
}
@@ -108,7 +113,7 @@ class ChannelInfo extends StatelessWidget {
),
const SizedBox(width: 10),
Text(
'Searching for Network',
context.translations.searchingForNetworkText,
style: textStyle,
),
],
@@ -122,7 +127,7 @@ class ChannelInfo extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Offline...',
context.translations.offlineLabel,
style: textStyle,
),
TextButton(
@@ -138,7 +143,7 @@ class ChannelInfo extends StatelessWidget {
..closeConnection()
..openConnection(),
child: Text(
'Try Again',
context.translations.tryAgainLabel,
style: textStyle?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
@@ -103,22 +104,21 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
statusString = context.translations.connectedLabel;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
statusString = context.translations.reconnectingLabel;
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
statusString = context.translations.disconnectedLabel;
break;
}
final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions
showMessage: showConnectionStateTile ? showStatus : false,
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
textTheme: Theme.of(context).textTheme,
@@ -208,7 +208,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget _buildConnectedTitleState(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Text(
'Stream Chat',
context.translations.streamChatLabel,
style: chatThemeData.textTheme.headlineBold.copyWith(
color: chatThemeData.colorTheme.textHighEmphasis,
),
@@ -227,7 +227,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
),
const SizedBox(width: 10),
Text(
'Searching for Network',
context.translations.searchingForNetworkText,
style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -245,7 +245,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Offline...',
context.translations.offlineLabel,
style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -256,7 +256,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
..closeConnection()
..openConnection(),
child: Text(
'Try Again',
context.translations.tryAgainLabel,
style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -8,6 +8,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget?);
@@ -302,7 +303,7 @@ class _ChannelListViewState extends State<ChannelListView> {
Padding(
padding: const EdgeInsets.all(8),
child: Text(
'Lets start chatting!',
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
),
@@ -312,7 +313,7 @@ class _ChannelListViewState extends State<ChannelListView> {
horizontal: 52,
),
child: Text(
'How about sending your first message to a friend?',
context.translations.sendingFirstMessageLabel,
textAlign: TextAlign.center,
style: chatThemeData.textTheme.body.copyWith(
color: chatThemeData.colorTheme.textLowEmphasis,
@@ -331,7 +332,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: TextButton(
onPressed: widget.onStartChatPressed,
child: Text(
'Start a chat',
context.translations.startAChatLabel,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentPrimary,
),
@@ -467,9 +468,9 @@ class _ChannelListViewState extends State<ChannelListView> {
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
const TextSpan(
TextSpan(
children: [
WidgetSpan(
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(
right: 2,
@@ -477,14 +478,14 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
),
TextButton(
onPressed: () => _channelListController.loadData!(),
child: const Text('Retry'),
child: Text(context.translations.retryLabel),
),
],
),
@@ -562,17 +563,16 @@ class _ChannelListViewState extends State<ChannelListView> {
),
onTap: widget.onDeletePressed != null
? () {
widget.onDeletePressed!(channel);
widget.onDeletePressed?.call(channel);
}
: () async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
title: context.translations.deleteConversationLabel,
question:
// ignore: lines_longer_than_80_chars
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
context.translations.deleteConversationQuestion,
okText: context.translations.deleteLabel,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError,
),
@@ -669,7 +669,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Error loading channels',
context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith(
color: Colors.white,
),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// It shows the current [Channel] name using a [Text] widget.
///
@@ -44,8 +45,10 @@ class ChannelName extends StatelessWidget {
) =>
LayoutBuilder(
builder: (context, constraints) {
var title = 'No title';
if (extraData['name'] == null) {
var title = context.translations.noTitleText;
if (extraData['name'] != null) {
title = extraData['name'];
} else {
final otherMembers = members
?.where((member) => member.userId != client.currentUser!.id);
if (otherMembers?.length == 1) {
@@ -71,8 +74,6 @@ class ChannelName extends StatelessWidget {
title = '${currentMembers.map((e) => e.user?.name).join(', ')} '
'${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
}
} else {
title = extraData['name'];
}
return Text(
@@ -3,6 +3,7 @@ import 'package:collection/collection.dart'
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
@@ -173,7 +174,7 @@ class ChannelPreview extends StatelessWidget {
startOfDay
.subtract(const Duration(days: 1))
.millisecondsSinceEpoch) {
stringDate = 'Yesterday';
stringDate = context.translations.yesterdayLabel;
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
@@ -199,7 +200,7 @@ class ChannelPreview extends StatelessWidget {
size: 16,
),
Text(
' Channel is muted',
' ${context.translations.channelIsMutedText}',
style: chatThemeData.channelPreviewTheme.subtitleStyle,
),
],
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget {
@@ -24,10 +25,10 @@ class DateDivider extends StatelessWidget {
String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'Today';
dayInfo = context.translations.todayLabel;
} else if (Jiffy(createdAt)
.isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
dayInfo = 'Yesterday';
dayInfo = context.translations.yesterdayLabel;
} else if (Jiffy(createdAt).isAfter(
now.subtract(const Duration(days: 7)),
Units.DAY,
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
@@ -50,7 +51,7 @@ class DeletedMessage extends StatelessWidget {
horizontal: 16,
),
child: Text(
'Message deleted',
context.translations.messageDeletedLabel,
style: messageTheme.messageTextStyle?.copyWith(
fontStyle: FontStyle.italic,
color: messageTheme.createdAtStyle?.color,
@@ -2,6 +2,7 @@ import 'package:characters/characters.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/localization/translations.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
final _emojiChars = Emoji.chars();
@@ -9,7 +10,8 @@ final _emojiChars = Emoji.chars();
/// String extension
extension StringExtension on String {
/// Returns the capitalized string
String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
String capitalize() =>
'${this[0].toUpperCase()}${substring(1).toLowerCase()}';
/// Returns whether the string contains only emoji's or not.
///
@@ -103,6 +105,11 @@ extension BuildContextX on BuildContext {
// ignore: public_member_api_docs
double get textScaleFactor =>
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0;
/// Retrieves current translations according to locale
/// Defaults to [DefaultTranslations]
Translations get translations =>
StreamChatLocalizations.of(this) ?? DefaultTranslations.instance;
}
/// Extension on [BorderRadius]
@@ -4,12 +4,12 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/gallery_footer.dart';
import 'package:stream_chat_flutter/src/gallery_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Return action for coming back from pages
enum ReturnActionType {
@@ -181,9 +181,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
children: [
GalleryHeader(
userName: widget.userName,
sentAt:
// ignore: lines_longer_than_80_chars
'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}',
sentAt: context.translations.sentAtText(
date: widget.message.createdAt,
time: widget.message.createdAt,
),
onBackPressed: () {
Navigator.of(context).pop();
},
@@ -196,7 +197,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
);
},
),
if (widget.message.type != 'ephemeral')
if (!widget.message.isEphemeral)
GalleryFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
@@ -221,22 +222,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
),
);
String getDay(DateTime dateTime) {
final now = DateTime.now();
if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
DateTime(now.year, now.month, now.day)) {
return 'today';
} else if (DateTime(now.year, now.month, now.day)
.difference(dateTime)
.inHours <
24) {
return 'yesterday';
} else {
return 'on ${Jiffy(dateTime).MMMd}';
}
}
@override
void dispose() async {
for (final package in videoPackages.values) {
@@ -12,6 +12,7 @@ import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Footer widget for media display
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
@@ -136,7 +137,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
'${widget.currentPage + 1} '
'${context.translations.ofText} '
'${widget.totalPages}',
style: galleryFooterThemeData.titleTextStyle,
),
],
@@ -192,7 +195,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Photos',
context.translations.photosLabel,
style:
galleryFooterThemeData.bottomSheetPhotosTextStyle,
),
@@ -68,7 +68,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
: const SizedBox(),
backgroundColor: galleryHeaderThemeData.backgroundColor,
actions: <Widget>[
if (message.type != 'ephemeral')
if (!message.isEphemeral)
IconButton(
icon: StreamSvgIcon.iconMenuPoint(
color: galleryHeaderThemeData.iconMenuPointColor,
@@ -79,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
),
],
centerTitle: true,
title: message.type != 'ephemeral'
title: !message.isEphemeral
? InkWell(
onTap: onTitleTap,
child: SizedBox(
@@ -0,0 +1,36 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/localization/translations.dart'
show Translations;
/// Defines the localized resource values used by the StreamChatFlutter widgets.
///
/// See also:
///
/// * [GlobalStreamChatLocalizations], which provides stream chat localizations
/// for many languages.
abstract class StreamChatLocalizations implements Translations {
/// The `StreamChatLocalizations` from the closest [Localizations] instance
/// that encloses the given context.
///
/// If no [StreamChatLocalizations] are available in the given `context`, this
/// method returns null.
///
/// This method is just a convenient shorthand for:
/// `Localizations.of<StreamChatLocalizations>(
/// context,
/// StreamChatLocalizations
/// )`.
///
/// References to the localized resources defined by this class are typically
/// written in terms of this method. For example:
///
/// ```dart
/// tooltip: StreamChatLocalizations.of(context).streamChatLabel,
/// ```
static StreamChatLocalizations? of(BuildContext context) =>
Localizations.of<StreamChatLocalizations>(
context,
StreamChatLocalizations,
);
}
@@ -0,0 +1,667 @@
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/connection_status_builder.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/message_search_list_view.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
show User;
/// Translation strings for the stream chat widgets
abstract class Translations {
/// The error shown when [launchURL] fails
String get launchUrlError;
/// The error shown when loading users fails
String get loadingUsersError;
/// The label for "retry" button
String get retryLabel;
/// The label for showing no users
String get noUsersLabel;
/// The text for showing user is online
String get userOnlineText;
/// The text for showing the last online of the user
String get userLastOnlineText;
/// The text shown when [users] starts typing
String userTypingText(Iterable<User> users);
/// The label for "thread reply"
String get threadReplyLabel;
/// The text for showing if the message is only visible to you
String get onlyVisibleToYouText;
/// The text for showing the thread reply count
String threadReplyCountText(int count);
/// The text for showing the attachments upload progress
String attachmentsUploadProgressText({
required int remaining,
required int total,
});
/// The text for showing who pinned the message
String pinnedByUserText({
required User pinnedBy,
required User currentUser,
});
/// The text for showing there are empty messages
String get emptyMessagesText;
/// The text for showing generic error
String get genericErrorText;
/// The error shown when loading messages fails
String get loadingMessagesError;
/// The text for showing the result count in [MessageSearchListView]
String resultCountText(int count);
/// The text for showing the message is deleted
String get messageDeletedText;
/// The label for message deleted
String get messageDeletedLabel;
/// The label for message reactions
String get messageReactionsLabel;
/// The text for showing there are no chats
String get emptyChatMessagesText;
/// The text for showing the thread separator in case [MessageListView]
/// contains a parent message
String threadSeparatorText(int replyCount);
/// The label for "connected" in [ConnectionStatusBuilder]
String get connectedLabel;
/// The label for "disconnected" in [ConnectionStatusBuilder]
String get disconnectedLabel;
/// The label for "reconnecting" in [ConnectionStatusBuilder]
String get reconnectingLabel;
/// The label for also send as direct message "checkbox"" in [MessageInput]
String get alsoSendAsDirectMessageLabel;
/// The label for search Gif
String get searchGifLabel;
/// The label for add a comment or send in case of
/// attachments inside [MessageInput]
String get addACommentOrSendLabel;
/// The label for write a message in [MessageInput]
String get writeAMessageLabel;
/// The label for instant commands in [MessageInput]
String get instantCommandsLabel;
/// The error shown in case the fi"le is too large even after compression
/// while uploading via [MessageInput]
String fileTooLargeAfterCompressionError(double limitInMB);
/// The error shown in case the file is too large
/// while uploading via [MessageInput]
String fileTooLargeError(double limitInMB);
/// The text for showing the query while searching for emojis
String emojiMatchingQueryText(String query);
/// The label for "add a file"
String get addAFileLabel;
/// The label for "upload a photo"
String get uploadAPhotoLabel;
/// The label for "upload a video"
String get uploadAVideoLabel;
/// The label for "photo from camera"
String get photoFromCameraLabel;
/// The label for "video from camera"
String get videoFromCameraLabel;
/// The label for "upload a file"
String get uploadAFileLabel;
/// The error shown when something went wrong
String get somethingWentWrongError;
/// The label for "OK"
String get okLabel;
/// The label for "add more files"
String get addMoreFilesLabel;
/// The message shown for asking photo and video access permission
String get enablePhotoAndVideoAccessMessage;
/// The message shown for asking gallery access permission
String get allowGalleryAccessMessage;
/// The label for "flag message"
String get flagMessageLabel;
/// The question asked while showing flag message dialog
String get flagMessageQuestion;
/// The label for "Flag"
String get flagLabel;
/// The label for "Cancel"
String get cancelLabel;
/// The label for successful message flag
String get flagMessageSuccessfulLabel;
/// The text for showing the message if successfully flagged
String get flagMessageSuccessfulText;
/// The label for "delete message"
String get deleteMessageLabel;
/// The question asked while showing delete message dialog
String get deleteMessageQuestion;
/// The label for "Delete"
String get deleteLabel;
/// The text for showing the operation could not be completed
String get operationCouldNotBeCompletedText;
/// The label for "Reply"
String get replyLabel;
/// The text for showing pin/un-pin functionality in [MessageWidget]
/// based on [pinned]
String togglePinUnpinText({required bool pinned});
/// The text for showing delete/retry-delete based on [isDeleteFailed]
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed});
/// The label for "copy message"
String get copyMessageLabel;
/// The label for "edit message"
String get editMessageLabel;
/// The text for showing resend/resend-edited message
/// based on [isUpdateFailed]
String toggleResendOrResendEditedMessage({required bool isUpdateFailed});
/// The label for "Photos"
String get photosLabel;
/// The text for showing on which [date] and [time] the message was sent
String sentAtText({required DateTime date, required DateTime time});
/// The label for "Today"
String get todayLabel;
/// The label for "Yesterday"
String get yesterdayLabel;
/// The text for showing the channel is muted
String get channelIsMutedText;
/// The text for showing there is no title
String get noTitleText;
/// The label for "let's start chatting"
String get letsStartChattingLabel;
/// The label for sending the first message
String get sendingFirstMessageLabel;
/// The label for "start a chat"
String get startAChatLabel;
/// The error shown when loading channel fails
String get loadingChannelsError;
/// The label for "Delete conversation"
String get deleteConversationLabel;
/// The question asked while showing delete conversation dialog
String get deleteConversationQuestion;
/// The label for "Stream Chat"
String get streamChatLabel;
/// The text for showing searching for network
String get searchingForNetworkText;
/// The label for "Offline"
String get offlineLabel;
/// The label for "Try again"
String get tryAgainLabel;
/// The text for showing the members count based on [count]
String membersCountText(int count);
/// The text for showing the watchers count based on [count]
String watchersCountText(int count);
/// The label for "View Info"
String get viewInfoLabel;
/// The label for "Leave Group"
String get leaveGroupLabel;
/// The label for "Leave"
String get leaveLabel;
/// The label for "Leave conversation"
String get leaveConversationLabel;
/// The question asked while showing leave conversation dialog
String get leaveConversationQuestion;
/// The label for "Show in chat"
String get showInChatLabel;
/// The label for "Save Image"
String get saveImageLabel;
/// The label for "Save Video"
String get saveVideoLabel;
/// The label for "Upload Error"
String get uploadErrorLabel;
/// The label for "Giphy"
String get giphyLabel;
/// The label for "Shuffle"
String get shuffleLabel;
/// The label for "Send"
String get sendLabel;
/// The label for "With"
String get withText;
/// The text shown for "In"
String get inText;
/// The text shown for "You"
String get youText;
/// The text shown for "Of"
String get ofText;
/// The text shown for "File"
String get fileText;
/// The label for "Reply to message"
String get replyToMessageLabel;
}
/// Default implementation of Translation strings for the stream chat widgets
class DefaultTranslations implements Translations {
const DefaultTranslations._();
/// Singleton instance of [DefaultTranslations]
static const instance = DefaultTranslations._();
@override
String get launchUrlError => 'Cannot launch the url';
@override
String get loadingUsersError => 'Error loading users';
@override
String get noUsersLabel => 'There are no users currently';
@override
String get retryLabel => 'Retry';
@override
String get userLastOnlineText => 'Last online';
@override
String get userOnlineText => 'Online';
@override
String userTypingText(Iterable<User> users) {
if (users.isEmpty) return '';
final first = users.first;
if (users.length == 1) {
return '${first.name} is typing';
}
return '${first.name} and ${users.length - 1} more are typing';
}
@override
String get threadReplyLabel => 'Thread Reply';
@override
String get onlyVisibleToYouText => 'Only visible to you';
@override
String threadReplyCountText(int count) => '$count Thread Replies';
@override
String attachmentsUploadProgressText({
required int remaining,
required int total,
}) =>
'Uploading $remaining/$total ...';
@override
String pinnedByUserText({
required User pinnedBy,
required User currentUser,
}) {
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
if (pinnedByCurrentUser) return 'Pinned by You';
return 'Pinned by ${pinnedBy.name}';
}
@override
String get emptyMessagesText => 'There are no messages currently';
@override
String get genericErrorText => 'Something went wrong';
@override
String get loadingMessagesError => 'Error loading messages';
@override
String resultCountText(int count) => '$count results';
@override
String get messageDeletedText => 'This message is deleted.';
@override
String get messageDeletedLabel => 'Message deleted';
@override
String get messageReactionsLabel => 'Message Reactions';
@override
String get emptyChatMessagesText => 'No chats here yet...';
@override
String threadSeparatorText(int replyCount) {
if (replyCount == 1) return '1 Reply';
return '$replyCount Replies';
}
@override
String get connectedLabel => 'Connected';
@override
String get disconnectedLabel => 'Disconnected';
@override
String get reconnectingLabel => 'Reconnecting...';
@override
String get alsoSendAsDirectMessageLabel => 'Also send as direct message';
@override
String get addACommentOrSendLabel => 'Add a comment or send';
@override
String get searchGifLabel => 'Search GIFs';
@override
String get writeAMessageLabel => 'Write a message';
@override
String get instantCommandsLabel => 'Instant Commands';
@override
String fileTooLargeAfterCompressionError(double limitInMB) =>
'The file is too large to upload. '
'The file size limit is $limitInMB MB. '
'We tried compressing it, but it was not enough.';
@override
String fileTooLargeError(double limitInMB) =>
'The file is too large to upload. The file size limit is $limitInMB MB.';
@override
String emojiMatchingQueryText(String query) => 'Emoji matching "$query"';
@override
String get addAFileLabel => 'Add a file';
@override
String get photoFromCameraLabel => 'Photo from camera';
@override
String get uploadAFileLabel => 'Upload a file';
@override
String get uploadAPhotoLabel => 'Upload a photo';
@override
String get uploadAVideoLabel => 'Upload a video';
@override
String get videoFromCameraLabel => 'Video from camera';
@override
String get okLabel => 'OK';
@override
String get somethingWentWrongError => 'Something went wrong';
@override
String get addMoreFilesLabel => 'Add more files';
@override
String get enablePhotoAndVideoAccessMessage =>
'Please enable access to your photos'
'\nand videos so you can share them with friends.';
@override
String get allowGalleryAccessMessage => 'Allow access to your gallery';
@override
String get flagMessageLabel => 'Flag Message';
@override
String get flagMessageQuestion =>
'Do you want to send a copy of this message to a'
'\nmoderator for further investigation?';
@override
String get flagLabel => 'FLAG';
@override
String get cancelLabel => 'CANCEL';
@override
String get flagMessageSuccessfulLabel => 'Message flagged';
@override
String get flagMessageSuccessfulText =>
'The message has been reported to a moderator.';
@override
String get deleteLabel => 'DELETE';
@override
String get deleteMessageLabel => 'Delete Message';
@override
String get deleteMessageQuestion =>
'Are you sure you want to permanently delete this\nmessage?';
@override
String get operationCouldNotBeCompletedText =>
'The operation couldn\'t be completed.';
@override
String get replyLabel => 'Reply';
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Unpin from Conversation';
return 'Pin to Conversation';
}
@override
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
if (isDeleteFailed) return 'Retry Deleting Message';
return 'Delete Message';
}
@override
String get copyMessageLabel => 'Copy Message';
@override
String get editMessageLabel => 'Edit Message';
@override
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
if (isUpdateFailed) return 'Resend Edited Message';
return 'Resend';
}
@override
String get photosLabel => 'Photos';
String _getDay(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = DateTime(now.year, now.month, now.day - 1);
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (date == today) {
return 'today';
} else if (date == yesterday) {
return 'yesterday';
} else {
return 'on ${Jiffy(date).MMMd}';
}
}
@override
String sentAtText({required DateTime date, required DateTime time}) =>
'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}';
@override
String get todayLabel => 'Today';
@override
String get yesterdayLabel => 'Yesterday';
@override
String get channelIsMutedText => 'Channel is muted';
@override
String get noTitleText => 'No title';
@override
String get letsStartChattingLabel => 'Lets start chatting!';
@override
String get sendingFirstMessageLabel =>
'How about sending your first message to a friend?';
@override
String get startAChatLabel => 'Start a chat';
@override
String get loadingChannelsError => 'Error loading channels';
@override
String get deleteConversationLabel => 'Delete Conversation';
@override
String get deleteConversationQuestion =>
'Are you sure you want to delete this conversation?';
@override
String get streamChatLabel => 'Stream Chat';
@override
String get searchingForNetworkText => 'Searching for Network';
@override
String get offlineLabel => 'Offline...';
@override
String get tryAgainLabel => 'Try Again';
@override
String membersCountText(int count) {
if (count == 1) return '1 Member';
return '$count Members';
}
@override
String watchersCountText(int count) {
if (count == 1) return '1 Online';
return '$count Online';
}
@override
String get viewInfoLabel => 'View Info';
@override
String get leaveGroupLabel => 'Leave Group';
@override
String get leaveLabel => 'LEAVE';
@override
String get leaveConversationLabel => 'Leave conversation';
@override
String get leaveConversationQuestion =>
'Are you sure you want to leave this conversation?';
@override
String get showInChatLabel => 'Show in Chat';
@override
String get saveImageLabel => 'Save Image';
@override
String get saveVideoLabel => 'Save Video';
@override
String get uploadErrorLabel => 'UPLOAD ERROR';
@override
String get giphyLabel => 'Giphy';
@override
String get shuffleLabel => 'Shuffle';
@override
String get sendLabel => 'Send';
@override
String get withText => 'with';
@override
String get inText => 'in';
@override
String get youText => 'You';
@override
String get ofText => 'of';
@override
String get fileText => 'File';
@override
String get replyToMessageLabel => 'Reply to Message';
}
@@ -269,16 +269,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final streamChatThemeData = StreamChatTheme.of(context);
final answer = await showConfirmationDialog(
context,
title: 'Flag Message',
title: context.translations.flagMessageLabel,
icon: StreamSvgIcon.flag(
color: streamChatThemeData.colorTheme.accentError,
size: 24,
),
question:
// ignore: lines_longer_than_80_chars
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
okText: 'FLAG',
cancelText: 'CANCEL',
question: context.translations.flagMessageQuestion,
okText: context.translations.flagLabel,
cancelText: context.translations.cancelLabel,
);
final theme = streamChatThemeData;
@@ -291,9 +289,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: theme.colorTheme.accentError,
size: 24,
),
details: 'The message has been reported to a moderator.',
title: 'Message flagged',
okText: 'OK',
details: context.translations.flagMessageSuccessfulText,
title: context.translations.flagMessageSuccessfulLabel,
okText: context.translations.okLabel,
);
} catch (err) {
if (err is StreamChatNetworkError &&
@@ -304,9 +302,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: theme.colorTheme.accentError,
size: 24,
),
details: 'The message has been reported to a moderator.',
title: 'Message flagged',
okText: 'OK',
details: context.translations.flagMessageSuccessfulText,
title: context.translations.flagMessageSuccessfulLabel,
okText: context.translations.okLabel,
);
} else {
_showErrorAlert();
@@ -336,14 +334,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
});
final answer = await showConfirmationDialog(
context,
title: 'Delete message',
title: context.translations.deleteMessageLabel,
icon: StreamSvgIcon.flag(
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24,
),
question: 'Are you sure you want to permanently delete this\nmessage?',
okText: 'DELETE',
cancelText: 'CANCEL',
question: context.translations.deleteMessageQuestion,
okText: context.translations.deleteLabel,
cancelText: context.translations.cancelLabel,
);
if (answer == true) {
@@ -367,9 +365,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24,
),
details: 'The operation couldn\'t be completed.',
title: 'Something went wrong',
okText: 'OK',
details: context.translations.operationCouldNotBeCompletedText,
title: context.translations.somethingWentWrongError,
okText: context.translations.okLabel,
);
}
@@ -391,7 +389,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'Reply',
context.translations.replyLabel,
style: streamChatThemeData.textTheme.body,
),
],
@@ -413,7 +411,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'Flag Message',
context.translations.flagMessageLabel,
style: streamChatThemeData.textTheme.body,
),
],
@@ -436,7 +434,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'${widget.message.pinned ? 'Unpin from' : 'Pin to'} Conversation',
context.translations.togglePinUnpinText(
pinned: widget.message.pinned,
),
style: streamChatThemeData.textTheme.body,
),
],
@@ -459,7 +459,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message',
context.translations.toggleDeleteRetryDeleteMessageText(
isDeleteFailed: isDeleteFailed,
),
style: StreamChatTheme.of(context)
.textTheme
.body
@@ -488,7 +490,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'Copy Message',
context.translations.copyMessageLabel,
style: streamChatThemeData.textTheme.body,
),
],
@@ -513,7 +515,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'Edit Message',
context.translations.editMessageLabel,
style: streamChatThemeData.textTheme.body,
),
],
@@ -545,7 +547,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
isUpdateFailed ? 'Resend Edited Message' : 'Resend',
context.translations.toggleResendOrResendEditedMessage(
isUpdateFailed: isUpdateFailed,
),
style: streamChatThemeData.textTheme.body,
),
],
@@ -590,9 +594,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: streamChatThemeData.colorTheme.disabled,
),
),
const Text(
'Edit Message',
style: TextStyle(fontWeight: FontWeight.bold),
Text(
context.translations.editMessageLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
visualDensity: VisualDensity.compact,
@@ -638,7 +642,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
const SizedBox(width: 16),
Text(
'Thread Reply',
context.translations.threadReplyLabel,
style: streamChatThemeData.textTheme.body,
),
],
@@ -358,9 +358,9 @@ class MessageInputState extends State<MessageInput> {
color: _streamChatTheme.colorTheme.disabled,
),
),
const Text(
'Reply to Message',
style: TextStyle(fontWeight: FontWeight.bold),
Text(
context.translations.replyToMessageLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
visualDensity: VisualDensity.compact,
@@ -462,7 +462,7 @@ class MessageInputState extends State<MessageInput> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Also send as direct message',
context.translations.alsoSendAsDirectMessageLabel,
style: _streamChatTheme.textTheme.footnote.copyWith(
color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(0.5),
@@ -587,7 +587,7 @@ class MessageInputState extends State<MessageInput> {
style: _messageInputTheme.inputTextStyle,
autofocus: widget.autofocus,
textAlignVertical: TextAlignVertical.center,
decoration: _getInputDecoration(),
decoration: _getInputDecoration(context),
textCapitalization: TextCapitalization.sentences,
),
)
@@ -599,11 +599,11 @@ class MessageInputState extends State<MessageInput> {
);
}
InputDecoration _getInputDecoration() {
InputDecoration _getInputDecoration(BuildContext context) {
final passedDecoration = _messageInputTheme.inputDecoration;
return InputDecoration(
isDense: true,
hintText: _getHint(),
hintText: _getHint(context),
hintStyle: _messageInputTheme.inputTextStyle!.copyWith(
color: _streamChatTheme.colorTheme.textLowEmphasis,
),
@@ -752,14 +752,14 @@ class MessageInputState extends State<MessageInput> {
);
}
String _getHint() {
String _getHint(BuildContext context) {
if (_commandEnabled && _chosenCommand!.name == 'giphy') {
return 'Search GIFs';
return context.translations.searchGifLabel;
}
if (_attachments.isNotEmpty) {
return 'Add a comment or send';
return context.translations.addACommentOrSendLabel;
}
return 'Write a message';
return context.translations.writeAMessageLabel;
}
void _checkEmoji(String s, BuildContext context) {
@@ -882,7 +882,7 @@ class MessageInputState extends State<MessageInput> {
),
),
Text(
'Instant Commands',
context.translations.instantCommandsLabel,
style: TextStyle(
color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5),
@@ -1145,8 +1145,9 @@ class MessageInputState extends State<MessageInput> {
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
// ignore: lines_longer_than_80_chars
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
@@ -1157,9 +1158,9 @@ class MessageInputState extends State<MessageInput> {
path: mediaInfo.path,
);
} else {
_showErrorAlert(
'The file is too large to upload. The file size limit is 20MB.',
);
_showErrorAlert(context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
}
@@ -1427,7 +1428,9 @@ class MessageInputState extends State<MessageInput> {
),
Flexible(
child: Text(
'Emoji matching "$query"',
context.translations.emojiMatchingQueryText(
query,
),
style: TextStyle(
color: _streamChatTheme
.colorTheme.textHighEmphasis
@@ -1777,17 +1780,17 @@ class MessageInputState extends State<MessageInput> {
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ListTile(
ListTile(
title: Text(
'Add a file',
style: TextStyle(
context.translations.addAFileLabel,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
ListTile(
leading: const Icon(Icons.image),
title: const Text('Upload a photo'),
title: Text(context.translations.uploadAPhotoLabel),
onTap: () {
pickFile(DefaultAttachmentTypes.image);
Navigator.pop(context);
@@ -1795,7 +1798,7 @@ class MessageInputState extends State<MessageInput> {
),
ListTile(
leading: const Icon(Icons.video_library),
title: const Text('Upload a video'),
title: Text(context.translations.uploadAVideoLabel),
onTap: () {
pickFile(DefaultAttachmentTypes.video);
Navigator.pop(context);
@@ -1804,7 +1807,7 @@ class MessageInputState extends State<MessageInput> {
if (!kIsWeb)
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Photo from camera'),
title: Text(context.translations.photoFromCameraLabel),
onTap: () {
pickFile(DefaultAttachmentTypes.image, true);
Navigator.pop(context);
@@ -1813,7 +1816,7 @@ class MessageInputState extends State<MessageInput> {
if (!kIsWeb)
ListTile(
leading: const Icon(Icons.videocam),
title: const Text('Video from camera'),
title: Text(context.translations.videoFromCameraLabel),
onTap: () {
pickFile(DefaultAttachmentTypes.video, true);
Navigator.pop(context);
@@ -1821,7 +1824,7 @@ class MessageInputState extends State<MessageInput> {
),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: const Text('Upload a file'),
title: Text(context.translations.uploadAFileLabel),
onTap: () {
pickFile(DefaultAttachmentTypes.file);
Navigator.pop(context);
@@ -1924,8 +1927,9 @@ class MessageInputState extends State<MessageInput> {
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
// ignore: lines_longer_than_80_chars
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
@@ -1936,9 +1940,9 @@ class MessageInputState extends State<MessageInput> {
path: mediaInfo.path,
);
} else {
_showErrorAlert(
'The file is too large to upload. The file size limit is 20MB.',
);
_showErrorAlert(context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
}
@@ -2118,7 +2122,7 @@ class MessageInputState extends State<MessageInput> {
height: 26,
),
Text(
'Something went wrong',
context.translations.somethingWentWrongError,
style: _streamChatTheme.textTheme.headlineBold,
),
const SizedBox(
@@ -2147,7 +2151,7 @@ class MessageInputState extends State<MessageInput> {
Navigator.of(context).pop();
},
child: Text(
'OK',
context.translations.okLabel,
style: _streamChatTheme.textTheme.bodyBold.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary),
),
@@ -2259,10 +2263,10 @@ class _PickerWidget extends StatefulWidget {
final StreamChatThemeData streamChatTheme;
@override
__PickerWidgetState createState() => __PickerWidgetState();
_PickerWidgetState createState() => _PickerWidgetState();
}
class __PickerWidgetState extends State<_PickerWidget> {
class _PickerWidgetState extends State<_PickerWidget> {
Future<bool>? requestPermission;
@override
@@ -2294,7 +2298,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center,
child: Text(
'Add more files',
context.translations.addMoreFilesLabel,
style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
@@ -2326,8 +2330,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
color: widget.streamChatTheme.colorTheme.disabled,
),
Text(
// ignore: lines_longer_than_80_chars
'Please enable access to your photos \nand videos so you can share them with friends.',
context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith(
color:
widget.streamChatTheme.colorTheme.textLowEmphasis),
@@ -2336,7 +2339,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
const SizedBox(height: 6),
Center(
child: Text(
'Allow access to your gallery',
context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentPrimary,
),
@@ -336,6 +336,7 @@ class _MessageListViewState extends State<MessageListView> {
bool _inBetweenList = false;
late final _defaultController = MessageListController();
MessageListController get _messageListController =>
widget.messageListController ?? _defaultController;
@@ -350,7 +351,7 @@ class _MessageListViewState extends State<MessageListView> {
emptyBuilder: widget.emptyBuilder ??
(context) => Center(
child: Text(
'No chats here yet...',
context.translations.emptyChatMessagesText,
style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)),
@@ -363,7 +364,7 @@ class _MessageListViewState extends State<MessageListView> {
errorBuilder: widget.errorBuilder ??
(BuildContext context, Object error) => Center(
child: Text(
'Something went wrong',
context.translations.genericErrorText,
style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)),
@@ -409,14 +410,14 @@ class _MessageListViewState extends State<MessageListView> {
var showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
statusString = context.translations.connectedLabel;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
statusString = context.translations.reconnectingLabel;
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
statusString = context.translations.disconnectedLabel;
break;
}
@@ -618,7 +619,7 @@ class _MessageListViewState extends State<MessageListView> {
return widget.threadSeparatorBuilder!.call(context);
}
final replyCount = widget.parentMessage!.replyCount;
final replyCount = widget.parentMessage!.replyCount!;
return DecoratedBox(
decoration: BoxDecoration(
gradient: _streamTheme.colorTheme.bgGradient,
@@ -626,7 +627,7 @@ class _MessageListViewState extends State<MessageListView> {
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center,
style: _streamTheme.channelHeaderTheme.subtitleStyle,
),
@@ -965,7 +966,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id);
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
Widget messageWidget = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
@@ -1072,7 +1073,8 @@ class _MessageListViewState extends State<MessageListView> {
}
FocusScope.of(context).unfocus();
},
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role),
);
if (widget.messageBuilder != null) {
@@ -1250,8 +1252,8 @@ class _LoadingIndicator extends StatelessWidget {
initialData: false,
errorBuilder: (context, error) => Container(
color: streamTheme.colorTheme.accentError.withOpacity(.2),
child: const Center(
child: Text('Error loading messages'),
child: Center(
child: Text(context.translations.loadingMessagesError),
),
),
builder: (context, data) {
@@ -8,6 +8,7 @@ import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Modal widget for displaying message reactions
class MessageReactionsModal extends StatelessWidget {
@@ -156,7 +157,7 @@ class MessageReactionsModal extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Message Reactions',
context.translations.messageReactionsLabel,
style: chatThemeData.textTheme.headlineBold,
),
const SizedBox(height: 16),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// It shows the current [Message] preview.
///
@@ -50,13 +51,13 @@ class MessageSearchItem extends StatelessWidget {
children: [
Text(
user.id == StreamChat.of(context).currentUser?.id
? 'You'
? context.translations.youText
: user.name,
style: chatThemeData.channelPreviewTheme.titleStyle,
),
if (channelName != null) ...[
Text(
' in ',
' ${context.translations.inText} ',
style: chatThemeData.channelPreviewTheme.titleStyle?.copyWith(
fontWeight: FontWeight.normal,
),
@@ -100,7 +101,7 @@ class MessageSearchItem extends StatelessWidget {
Widget _buildSubtitle(BuildContext context, Message message) {
var text = message.text;
if (message.isDeleted) {
text = 'This message was deleted.';
text = context.translations.messageDeletedText;
} else if (message.attachments.isNotEmpty) {
final parts = <String>[
...message.attachments.map((e) {
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/message_search_item.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
@@ -141,6 +142,7 @@ class MessageSearchListView extends StatefulWidget {
class _MessageSearchListViewState extends State<MessageSearchListView> {
late final _defaultController = MessageSearchListController();
MessageSearchListController get _messageSearchListController =>
widget.messageSearchListController ?? _defaultController;
@@ -162,8 +164,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: Text('There are no messages currently'),
child: Center(
child: Text(context.translations.emptyMessagesText),
),
),
),
@@ -177,7 +179,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: 'An error occurred.',
message: context.translations.genericErrorText,
child: Container(),
);
},
@@ -241,10 +243,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
.colorTheme
.accentError
.withOpacity(.2),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('Error loading messages'),
child: Text(context.translations.loadingMessagesError),
),
),
);
@@ -307,7 +309,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
horizontal: 8,
),
child: Text(
'${items.length} results',
context.translations.resultCountText(items.length),
style: TextStyle(
color: chatThemeData.colorTheme.textLowEmphasis,
),
@@ -30,60 +30,66 @@ class MessageText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n');
final streamChat = StreamChat.of(context);
assert(streamChat.currentUser != null, '');
return BetterStreamBuilder<String>(
stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'),
initialData: streamChat.currentUser!.language ?? 'en',
builder: (context, language) {
final translatedText =
message.i18n?['${language}_text'] ?? message.text;
final messageText =
_replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n');
final themeData = Theme.of(context);
return MarkdownBody(
data: messageText,
onTapLink: (
String link,
String? href,
String title,
) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhereOrNull(
(u) => '@${u.name}' == link,
);
final themeData = Theme.of(context);
return MarkdownBody(
data: text,
onTapLink: (
String link,
String? href,
String title,
) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhereOrNull(
(u) => '@${u.name}' == link,
);
if (mentionedUser == null) {
return;
}
if (mentionedUser == null) return;
if (onMentionTap != null) {
onMentionTap!(mentionedUser);
} else {
print('tap on ${mentionedUser.name}');
}
} else {
if (onLinkTap != null) {
onLinkTap!(link);
} else {
launchURL(context, link);
}
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
themeData.copyWith(
textTheme: themeData.textTheme.apply(
bodyColor: messageTheme.messageTextStyle?.color,
decoration: messageTheme.messageTextStyle?.decoration,
decorationColor: messageTheme.messageTextStyle?.decorationColor,
decorationStyle: messageTheme.messageTextStyle?.decorationStyle,
fontFamily: messageTheme.messageTextStyle?.fontFamily,
onMentionTap?.call(mentionedUser);
} else {
if (onLinkTap != null) {
onLinkTap!(link);
} else {
launchURL(context, link);
}
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
themeData.copyWith(
textTheme: themeData.textTheme.apply(
bodyColor: messageTheme.messageTextStyle?.color,
decoration: messageTheme.messageTextStyle?.decoration,
decorationColor: messageTheme.messageTextStyle?.decorationColor,
decorationStyle: messageTheme.messageTextStyle?.decorationStyle,
fontFamily: messageTheme.messageTextStyle?.fontFamily,
),
),
).copyWith(
a: messageTheme.messageLinksStyle,
p: messageTheme.messageTextStyle,
),
),
).copyWith(
a: messageTheme.messageLinksStyle,
p: messageTheme.messageTextStyle,
),
);
},
);
}
String _replaceMentions(String text) {
message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) {
// ignore: parameter_assignments
text = text.replaceAll(
var messageTextToRender = text;
for (final user in message.mentionedUsers.toSet()) {
final userName = user.name;
messageTextToRender = messageTextToRender.replaceAll(
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
});
return text;
}
return messageTextToRender;
}
}
@@ -93,6 +93,8 @@ class MessageWidget extends StatefulWidget {
this.userAvatarBuilder,
this.editMessageInputBuilder,
this.textBuilder,
this.bottomRowBuilder,
this.deletedBottomRowBuilder,
this.onReturnAction,
Map<String, AttachmentBuilder>? customAttachmentBuilders,
this.readList,
@@ -276,6 +278,12 @@ class MessageWidget extends StatefulWidget {
/// Function called on long press
final void Function(BuildContext, Message)? onMessageActions;
/// Widget builder for building a bottom row below the message
final Widget Function(BuildContext, Message)? bottomRowBuilder;
/// Widget builder for building a bottom row below a deleted message
final Widget Function(BuildContext, Message)? deletedBottomRowBuilder;
/// Widget builder for building user avatar
final Widget Function(BuildContext, User)? userAvatarBuilder;
@@ -411,6 +419,8 @@ class MessageWidget extends StatefulWidget {
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,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
MessageThemeData? messageTheme,
@@ -464,6 +474,9 @@ class MessageWidget extends StatefulWidget {
editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme,
@@ -783,7 +796,11 @@ class _MessageWidgetState extends State<MessageWidget>
bottom:
isPinned && widget.showPinHighlight ? 6.0 : 0.0,
),
child: _bottomRow,
child: widget.bottomRowBuilder?.call(
context,
widget.message,
) ??
_bottomRow,
),
if (isFailedState)
Positioned(
@@ -831,22 +848,11 @@ class _MessageWidgetState extends State<MessageWidget>
Widget get _bottomRow {
if (isDeleted) {
final chatThemeData = _streamChatTheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: chatThemeData.colorTheme.textLowEmphasis,
size: 16,
),
const SizedBox(width: 8),
Text(
'Only visible to you',
style: chatThemeData.textTheme.footnote
.copyWith(color: chatThemeData.colorTheme.textLowEmphasis),
),
],
);
return widget.deletedBottomRowBuilder?.call(
context,
widget.message,
) ??
const Offstage();
}
final children = <Widget>[];
@@ -855,9 +861,9 @@ class _MessageWidgetState extends State<MessageWidget>
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
final replyCount = widget.message.replyCount;
var msg = 'Thread Reply';
var msg = context.translations.threadReplyLabel;
if (showThreadReplyIndicator && replyCount! > 1) {
msg = '$replyCount Thread Replies';
msg = context.translations.threadReplyCountText(replyCount);
}
// ignore: prefer_function_declarations_over_variables
@@ -1202,7 +1208,10 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
return Text(
'Uploading $uploadRemaining/$totalAttachments ...',
context.translations.attachmentsUploadProgressText(
remaining: uploadRemaining,
total: totalAttachments,
),
style: style,
);
}
@@ -1276,8 +1285,8 @@ class _MessageWidgetState extends State<MessageWidget>
}
Widget _buildPinnedMessage(Message message) {
final pinnedBy = message.pinnedBy;
final pinnedByMe = _streamChat.currentUser!.id == pinnedBy!.id;
final pinnedBy = message.pinnedBy!;
final currentUser = _streamChat.currentUser!;
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
@@ -1291,7 +1300,10 @@ class _MessageWidgetState extends State<MessageWidget>
width: 4,
),
Text(
'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}',
context.translations.pinnedByUserText(
pinnedBy: pinnedBy,
currentUser: currentUser,
),
style: TextStyle(
color: _streamChatTheme.colorTheme.textLowEmphasis,
fontSize: 13,
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -146,15 +145,14 @@ class StreamChatState extends State<StreamChat> {
/// The current user as a stream
Stream<User?> get currentUserStream => widget.client.state.currentUserStream;
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
final locale = ui.window.locale;
Jiffy.locale(locale.languageCode);
final currentLocale = Localizations.localeOf(context);
final languageCode = currentLocale.languageCode;
final availableLocales = Jiffy.getAllAvailableLocales();
if (availableLocales.contains(languageCode)) {
Jiffy.locale(languageCode);
}
super.didChangeDependencies();
}
}
@@ -13,8 +13,8 @@ class SystemMessage extends StatelessWidget {
/// This message
final Message message;
// ignore: lines_longer_than_80_chars
/// The function called when tapping on the message when the message is not failed
/// The function called when tapping on the message
/// when the message is not failed
final void Function(Message)? onMessageTap;
@override
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png)
@@ -111,7 +112,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'with ',
'${context.translations.withText} ',
style: chatThemeData.channelHeaderTheme.subtitleStyle,
),
Flexible(
@@ -148,7 +149,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
children: [
title ??
Text(
'Thread Reply',
context.translations.threadReplyLabel,
style: chatThemeData.channelHeaderTheme.titleStyle,
),
const SizedBox(height: 2),
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget {
@@ -63,8 +64,7 @@ class TypingIndicator extends StatelessWidget {
height: 4,
),
Text(
// ignore: lines_longer_than_80_chars
' ${data.elementAt(0).name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing',
context.translations.userTypingText(data),
maxLines: 1,
style: style,
),
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_list_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
///
/// It shows the current [User] preview.
@@ -86,12 +87,13 @@ class UserItem extends StatelessWidget {
);
}
Widget _buildLastActive(context) {
Widget _buildLastActive(BuildContext context) {
final chatTheme = StreamChatTheme.of(context);
return Text(
user.online == true
? 'Online'
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
? context.translations.userOnlineText
: '${context.translations.userLastOnlineText} '
'${Jiffy(user.lastActive).fromNow()}',
style: chatTheme.textTheme.footnote.copyWith(
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)),
);
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget?);
@@ -156,6 +157,7 @@ class _UserListViewState extends State<UserListView>
bool get _isListView => widget.crossAxisCount == 1;
late final _defaultController = UserListController();
UserListController get _userListController =>
widget.userListController ?? _defaultController;
@@ -221,9 +223,9 @@ class _UserListViewState extends State<UserListView>
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
const TextSpan(
TextSpan(
children: [
WidgetSpan(
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(
right: 2,
@@ -231,14 +233,14 @@ class _UserListViewState extends State<UserListView>
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading users'),
TextSpan(text: context.translations.loadingUsersError),
],
),
style: Theme.of(context).textTheme.headline6,
),
TextButton(
onPressed: () => _userListController.loadData!(),
child: const Text('Retry'),
child: Text(context.translations.retryLabel),
),
],
),
@@ -251,8 +253,8 @@ class _UserListViewState extends State<UserListView>
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: Text('There are no users currently'),
child: Center(
child: Text(context.translations.noUsersLabel),
),
),
),
@@ -401,10 +403,10 @@ class _UserListViewState extends State<UserListView>
.colorTheme
.accentError
.withOpacity(.2),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('Error loading users'),
child: Text(context.translations.loadingUsersError),
),
),
);
@@ -4,17 +4,15 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Launch URL
Future<void> launchURL(BuildContext context, String? url) async {
if (url != null && await canLaunch(url)) {
await launch(url);
} else {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
const SnackBar(
content: Text('Cannot launch the url'),
),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.translations.launchUrlError)),
);
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget for displaying a footnote
class VisibleFootnote extends StatelessWidget {
/// Constructor for creating a [VisibleFootnote]
const VisibleFootnote({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: chatThemeData.colorTheme.textLowEmphasis,
size: 16,
),
const SizedBox(width: 8),
Text(
context.translations.onlyVisibleToYouText,
style: chatThemeData.textTheme.footnote
.copyWith(color: chatThemeData.colorTheme.textLowEmphasis),
),
],
);
}
}
@@ -1,3 +1,4 @@
export 'package:jiffy/jiffy.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
@@ -16,6 +17,8 @@ export 'src/gallery_footer.dart';
export 'src/gallery_header.dart';
export 'src/gradient_avatar.dart';
export 'src/info_tile.dart';
export 'src/localization/stream_chat_localizations.dart';
export 'src/localization/translations.dart' show DefaultTranslations;
export 'src/mention_tile.dart';
export 'src/message_action.dart';
export 'src/message_input.dart';
@@ -41,3 +44,4 @@ export 'src/user_avatar.dart';
export 'src/user_item.dart';
export 'src/user_list_view.dart';
export 'src/utils.dart';
export 'src/visible_footnote.dart';
+4 -2
View File
@@ -1,12 +1,13 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 2.0.0
version: 2.1.2
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
cached_network_image: ^3.0.0
@@ -36,7 +37,7 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0
share_plus: ^2.0.3
shimmer: ^2.0.0
stream_chat_flutter_core: ^2.0.0
stream_chat_flutter_core: ^2.1.1
substring_highlight: ^1.0.26
synchronized: ^3.0.0
url_launcher: ^6.0.3
@@ -58,5 +59,6 @@ dev_dependencies:
sdk: flutter
golden_toolkit: ^0.9.0
mocktail: ^0.1.2
path: ^1.8.0
pedantic: ^1.11.0
@@ -0,0 +1,174 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void main() {
test('Default translations should exist', () {
const translations = DefaultTranslations.instance;
expect(translations.launchUrlError, isNotNull);
expect(translations.loadingUsersError, isNotNull);
expect(translations.noUsersLabel, isNotNull);
expect(translations.retryLabel, isNotNull);
expect(translations.userLastOnlineText, isNotNull);
expect(translations.userOnlineText, isNotNull);
expect(translations.userOnlineText, isNotNull);
// no users
expect(translations.userTypingText([]), isNotNull);
// single user
expect(translations.userTypingText([User(id: 'test-id')]), isNotNull);
// multiple users
expect(
translations.userTypingText([
User(id: 'test-id-1'),
User(id: 'test-id-2'),
]),
isNotNull,
);
expect(translations.threadReplyLabel, isNotNull);
expect(translations.onlyVisibleToYouText, isNotNull);
expect(translations.threadReplyCountText(3), isNotNull);
expect(
translations.attachmentsUploadProgressText(remaining: 3, total: 10),
isNotNull,
);
expect(
translations.pinnedByUserText(
pinnedBy: User(id: 'pinned-by-user-id'),
currentUser: OwnUser(id: 'current-user-id'),
),
isNotNull,
);
expect(translations.emptyMessagesText, isNotNull);
expect(translations.genericErrorText, isNotNull);
expect(translations.loadingMessagesError, isNotNull);
expect(translations.resultCountText(3), isNotNull);
expect(translations.messageDeletedText, isNotNull);
expect(translations.messageDeletedLabel, isNotNull);
expect(translations.messageReactionsLabel, isNotNull);
expect(translations.emptyChatMessagesText, isNotNull);
expect(translations.threadSeparatorText(3), isNotNull);
expect(translations.connectedLabel, isNotNull);
expect(translations.disconnectedLabel, isNotNull);
expect(translations.reconnectingLabel, isNotNull);
expect(translations.alsoSendAsDirectMessageLabel, isNotNull);
expect(translations.addACommentOrSendLabel, isNotNull);
expect(translations.searchGifLabel, isNotNull);
expect(translations.writeAMessageLabel, isNotNull);
expect(translations.instantCommandsLabel, isNotNull);
expect(translations.fileTooLargeAfterCompressionError(33), isNotNull);
expect(translations.fileTooLargeError(33), isNotNull);
expect(translations.emojiMatchingQueryText('sahil'), isNotNull);
expect(translations.addAFileLabel, isNotNull);
expect(translations.photoFromCameraLabel, isNotNull);
expect(translations.uploadAFileLabel, isNotNull);
expect(translations.uploadAPhotoLabel, isNotNull);
expect(translations.uploadAVideoLabel, isNotNull);
expect(translations.videoFromCameraLabel, isNotNull);
expect(translations.okLabel, isNotNull);
expect(translations.somethingWentWrongError, isNotNull);
expect(translations.addMoreFilesLabel, isNotNull);
expect(translations.enablePhotoAndVideoAccessMessage, isNotNull);
expect(translations.allowGalleryAccessMessage, isNotNull);
expect(translations.flagMessageLabel, isNotNull);
expect(translations.flagMessageQuestion, isNotNull);
expect(translations.flagLabel, isNotNull);
expect(translations.cancelLabel, isNotNull);
expect(translations.flagMessageSuccessfulLabel, isNotNull);
expect(translations.flagMessageSuccessfulText, isNotNull);
expect(translations.deleteLabel, isNotNull);
expect(translations.deleteMessageLabel, isNotNull);
expect(translations.deleteMessageQuestion, isNotNull);
expect(translations.operationCouldNotBeCompletedText, isNotNull);
expect(translations.replyLabel, isNotNull);
// pinned
expect(translations.togglePinUnpinText(pinned: true), isNotNull);
// un-pinned
expect(translations.togglePinUnpinText(pinned: false), isNotNull);
// delete-failed
expect(
translations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: true),
isNotNull,
);
// first-delete
expect(
translations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: false),
isNotNull,
);
expect(translations.copyMessageLabel, isNotNull);
expect(translations.editMessageLabel, isNotNull);
// resend-failed
expect(
translations.toggleResendOrResendEditedMessage(isUpdateFailed: true),
isNotNull,
);
// first resend
expect(
translations.toggleResendOrResendEditedMessage(isUpdateFailed: false),
isNotNull,
);
expect(translations.photosLabel, isNotNull);
// today
expect(
translations.sentAtText(
date: DateTime.now(),
time: DateTime.now(),
),
isNotNull,
);
// yesterday
expect(
translations.sentAtText(
date: DateTime.now().subtract(const Duration(days: 1)),
time: DateTime.now(),
),
isNotNull,
);
// any other day
expect(
translations.sentAtText(
date: DateTime.now().subtract(const Duration(days: 3)),
time: DateTime.now(),
),
isNotNull,
);
expect(translations.todayLabel, isNotNull);
expect(translations.yesterdayLabel, isNotNull);
expect(translations.channelIsMutedText, isNotNull);
expect(translations.noTitleText, isNotNull);
expect(translations.letsStartChattingLabel, isNotNull);
expect(translations.sendingFirstMessageLabel, isNotNull);
expect(translations.startAChatLabel, isNotNull);
expect(translations.loadingChannelsError, isNotNull);
expect(translations.deleteConversationLabel, isNotNull);
expect(translations.deleteConversationQuestion, isNotNull);
expect(translations.streamChatLabel, isNotNull);
expect(translations.searchingForNetworkText, isNotNull);
expect(translations.offlineLabel, isNotNull);
expect(translations.tryAgainLabel, isNotNull);
// 1 member
expect(translations.membersCountText(1), isNotNull);
// 3 members
expect(translations.membersCountText(3), isNotNull);
// 1 member
expect(translations.watchersCountText(1), isNotNull);
// 3 members
expect(translations.watchersCountText(3), isNotNull);
expect(translations.viewInfoLabel, isNotNull);
expect(translations.leaveGroupLabel, isNotNull);
expect(translations.leaveLabel, isNotNull);
expect(translations.leaveConversationLabel, isNotNull);
expect(translations.leaveConversationQuestion, isNotNull);
expect(translations.showInChatLabel, isNotNull);
expect(translations.saveImageLabel, isNotNull);
expect(translations.saveVideoLabel, isNotNull);
expect(translations.uploadErrorLabel, isNotNull);
expect(translations.giphyLabel, isNotNull);
expect(translations.shuffleLabel, isNotNull);
expect(translations.sendLabel, isNotNull);
expect(translations.withText, isNotNull);
expect(translations.inText, isNotNull);
expect(translations.youText, isNotNull);
expect(translations.ofText, isNotNull);
expect(translations.fileText, isNotNull);
expect(translations.replyToMessageLabel, isNotNull);
});
}
@@ -717,7 +717,7 @@ void main() {
await tester.tap(find.text('Delete Message'));
await tester.pumpAndSettle();
expect(find.text('Delete message'), findsOneWidget);
expect(find.text('Delete Message'), findsOneWidget);
await tester.tap(find.text('DELETE'));
await tester.pumpAndSettle();
@@ -773,7 +773,7 @@ void main() {
await tester.tap(find.text('Delete Message'));
await tester.pumpAndSettle();
expect(find.text('Delete message'), findsOneWidget);
expect(find.text('Delete Message'), findsOneWidget);
await tester.tap(find.text('DELETE'));
await tester.pumpAndSettle();
@@ -8,10 +8,33 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'mocks.dart';
import 'simple_frame.dart';
void expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
var currentString = 0;
for (final widget in widgets) {
if (widget is RichText) {
final span = widget.text as TextSpan;
final text = _extractTextFromTextSpan(span);
expect(text, equals(strings[currentString]));
currentString += 1;
}
}
}
String _extractTextFromTextSpan(TextSpan span) {
var text = span.text ?? '';
if (span.children != null) {
for (final child in span.children! as Iterable<TextSpan>) {
text += _extractTextFromTextSpan(child);
}
}
return text;
}
void main() {
testWidgets(
'it should show correct message text',
(WidgetTester tester) async {
final currentUser = OwnUser(id: 'user-id');
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
@@ -21,7 +44,9 @@ void main() {
final streamTheme = StreamChatThemeData.fromTheme(themeData);
when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.currentUser).thenReturn(currentUser);
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(currentUser));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -54,9 +79,107 @@ void main() {
},
);
group('Message with i18n field', () {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
const messageTheme = MessageThemeData();
final currentUser = OwnUser(
id: 'sahil',
language: 'hi',
);
setUp(() {
when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(currentUser);
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(currentUser));
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
when(() => channel.isMuted).thenReturn(false);
when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false));
});
testWidgets(
'should show correct translated message text as per user language',
(WidgetTester tester) async {
final message = Message(
text: 'Hello',
i18n: const {
'en_text': 'Hello',
'hi_text': 'नमस्ते',
'language': 'en',
},
);
await tester.pumpWidget(
MaterialApp(
home: StreamChat(
client: client,
child: StreamChannel(
channel: channel,
child: Scaffold(
body: MessageText(
message: message,
messageTheme: messageTheme,
),
),
),
),
),
);
expect(find.byType(MarkdownBody), findsOneWidget);
final widgets = tester.allWidgets;
expectTextStrings(widgets, <String>['नमस्ते']);
},
);
testWidgets(
'''should show default text if i18n does not contain translations as per user language''',
(WidgetTester tester) async {
final message = Message(
text: 'Hello',
i18n: const {
'en_text': 'Hello',
'fr_text': 'Bonjour',
'language': 'en',
},
);
await tester.pumpWidget(
MaterialApp(
home: StreamChat(
client: client,
child: StreamChannel(
channel: channel,
child: Scaffold(
body: MessageText(
message: message,
messageTheme: messageTheme,
),
),
),
),
),
);
expect(find.byType(MarkdownBody), findsOneWidget);
final widgets = tester.allWidgets;
expectTextStrings(widgets, <String>['Hello']);
},
);
});
testGoldens(
'control test',
(WidgetTester tester) async {
final currentUser = OwnUser(id: 'user-id');
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
@@ -66,7 +189,9 @@ void main() {
final streamTheme = StreamChatThemeData.fromTheme(themeData);
when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.currentUser).thenReturn(currentUser);
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(currentUser));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -90,14 +215,18 @@ cool.''';
await tester.pumpWidgetBuilder(
materialAppWrapper()(SimpleFrame(
child: StreamChannel(
channel: channel,
child: Scaffold(
body: MessageText(
message: Message(
text: messageText,
child: StreamChat(
client: client,
connectivityStream: Stream.value(ConnectivityResult.wifi),
child: StreamChannel(
channel: channel,
child: Scaffold(
body: MessageText(
message: Message(
text: messageText,
),
messageTheme: streamTheme.otherMessageTheme,
),
messageTheme: streamTheme.otherMessageTheme,
),
),
),
@@ -0,0 +1,58 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:golden_toolkit/src/testing_tools.dart';
import 'package:path/path.dart' as path;
const double _kGoldenDiffTolerance = 0.05;
/// Wrapper function for golden tests.
Future<void> customExpectGoldenMatches(
WidgetTester tester,
String name, {
bool? autoHeight,
Finder? finder,
CustomPump? customPump,
@Deprecated('''
This method level parameter will be removed in an upcoming release. This can be configured globally. If you have concerns, please file an issue with your use case.''') bool? skip,
}) {
final goldenPath = path.join('test/src/goldens');
print('goldenPath: $goldenPath');
goldenFileComparator = CustomGoldenFileComparator(Uri.parse(goldenPath));
return compareWithGolden(
tester,
name,
autoHeight: autoHeight,
finder: finder,
customPump: customPump,
skip: skip,
// This value is actually ignored. We are forced to pass it because the
// downstream API is structured poorly. This should be refactored.
device: Device.phone,
fileNameFactory: (String name, Device device) =>
GoldenToolkit.configuration.fileNameFactory(name),
);
}
class CustomGoldenFileComparator extends LocalFileComparator {
CustomGoldenFileComparator(Uri testFile) : super(testFile);
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
print('golden.toString(): ${golden.toString()}');
final result = await GoldenFileComparator.compareLists(
imageBytes,
await getGoldenBytes(golden),
);
if (!result.passed && result.diffPercent > _kGoldenDiffTolerance) {
final error = await generateFailureOutput(result, golden, basedir);
throw FlutterError(error);
}
return result.passed;
}
}